
*** Día 16/03/2026

Clave equipo: cursom
clave baño: 2017
Eva


Canal de reclamaciones/denuncias
Front donde cualquier persona puede poner una denuncia (se admiten anónimos) mediante un formulario
A los anónimos se les da un código para identificar su denuncia

A la empresa le llega la denuncia
El usuario puede ver la evolución de su queja
Por LOPD, cuantos menos datos guardemos del denunciante mejor

15 minutos de descanso y comida de 13:30 a 14:30

https://code.visualstudio.com/Download

PARTIDA:
{
    "name": "My Server",
    "host": "localhost",
    "protocol": "sftp",
    "port": 22,
    "username": "username",
    "remotePath": "/",
    "uploadOnSave": false,
    "useTempFile": false,
    "openSsh": false
}


DESTINO:
{
    "name": "arturo.avisando.com",
    "host": "arturo.avisando.com",
    "protocol": "sftp",
    "port": 22,
    "username": "arturo",
    "remotePath": "/home/arturo/public_html",
    "uploadOnSave": true,
    "useTempFile": false,
    "openSsh": false
}



arturo@123456

https://javier.avisando.com/php/examen2/examen2.zip tiene mucho contenido, me quedo con https://jose.avisando.com/examenes/2/2.zip de Jose
https://jose.avisando.com/jubiletas/jubiletas.zip es demasiado grande para mis intereses
avisando.com también existe, pero no la recojo

O bueno, pruebo con mi amigo grok:

Genérame un canal de reclamaciones/denuncias en PHP, con un front donde cualquier persona puede poner una denuncia (se admiten anónimos) mediante un formulario. A esos anónimos se les dará un código para identificar su denuncia. A la empresa le llegará la denuncia y el usuario puede ver la evolución de su queja


HTML:
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canal de Reclamaciones - MiEmpresa</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body { background: #f8f9fa; }
        .hero { background: linear-gradient(135deg, #0d6efd, #6610f2); color: white; padding: 80px 0; }
    </style>
</head>
<body>

<!-- ======================= ARCHIVO 1: config.php ======================= -->
<?php
// config.php
$host = 'localhost';
$db   = 'reclamaciones_db';
$user = 'root';
$pass = '';                     // Cambia según tu entorno
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
    die("Error de conexión: " . $e->getMessage());
}

// Correo de la empresa (cámbialo por el real)
define('EMAIL_EMPRESA', 'atencion@tuempresa.com');
?>

<!-- ======================= ARCHIVO 2: database.sql ======================= -->
```sql
-- database.sql (ejecuta esto en phpMyAdmin o MySQL)
CREATE DATABASE IF NOT EXISTS reclamaciones_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE reclamaciones_db;

CREATE TABLE IF NOT EXISTS complaints (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    code            VARCHAR(20) UNIQUE NOT NULL,
    name            VARCHAR(100) DEFAULT NULL,
    email           VARCHAR(100) DEFAULT NULL,
    subject         VARCHAR(200) NOT NULL,
    description     TEXT NOT NULL,
    status          ENUM('Pendiente', 'En proceso', 'Resuelta', 'Rechazada') DEFAULT 'Pendiente',
    admin_notes     TEXT DEFAULT NULL,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Índices para búsquedas rápidas
CREATE INDEX idx_code ON complaints(code);
CREATE INDEX idx_status ON complaints(status);


PHP
<?php
// index.php
require_once 'config.php';
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canal de Reclamaciones - MiEmpresa</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>body{background:#f8f9fa;}.hero{background:linear-gradient(135deg,#0d6efd,#6610f2);color:white;padding:80px 0;}</style>
</head>
<body>
<div class="hero text-center">
    <div class="container">
        <h1 class="display-4">Canal de Reclamaciones</h1>
        <p class="lead">Presenta tu denuncia o queja. Se admiten denuncias anónimas.</p>
        <a href="#formulario" class="btn btn-light btn-lg">Presentar denuncia ahora</a>
    </div>
</div>

<div class="container mt-5" id="formulario">
    <div class="row justify-content-center">
        <div class="col-lg-8">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h4 class="mb-0">Formulario de denuncia</h4>
                </div>
                <div class="card-body">
                    <form action="process.php" method="POST">
                        <div class="mb-3">
                            <label class="form-label">Nombre (opcional - si dejas en blanco será anónimo)</label>
                            <input type="text" name="name" class="form-control" placeholder="Juan Pérez">
                        </div>
                        
                        <div class="mb-3">
                            <label class="form-label">Email (opcional - solo si quieres recibir confirmación)</label>
                            <input type="email" name="email" class="form-control" placeholder="tu@email.com">
                        </div>
                        
                        <div class="mb-3">
                            <label class="form-label">Asunto *</label>
                            <input type="text" name="subject" class="form-control" required>
                        </div>
                        
                        <div class="mb-3">
                            <label class="form-label">Descripción de la denuncia *</label>
                            <textarea name="description" class="form-control" rows="6" required></textarea>
                        </div>
                        
                        <button type="submit" class="btn btn-primary btn-lg w-100">Enviar denuncia</button>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <a href="track.php" class="btn btn-outline-secondary">Seguimiento de mi denuncia →</a>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>


PHP
<?php
// process.php
require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}

$name        = trim($_POST['name'] ?? '');
$email       = trim($_POST['email'] ?? '');
$subject     = trim($_POST['subject'] ?? '');
$description = trim($_POST['description'] ?? '');

if (empty($subject) || empty($description)) {
    die("Asunto y descripción son obligatorios.");
}

// Generar código único (siempre se genera)
$code = 'DEN-' . strtoupper(substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 8));

// Insertar en BD
$stmt = $pdo->prepare("INSERT INTO complaints (code, name, email, subject, description) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$code, $name ?: null, $email ?: null, $subject, $description]);

$id = $pdo->lastInsertId();

// Notificar a la empresa por email
$to = EMAIL_EMPRESA;
$subjectMail = "Nueva denuncia recibida - Código: $code";
$message = "Nueva denuncia:\n\n";
$message .= "Código: $code\n";
$message .= "Nombre: " . ($name ?: 'Anónimo') . "\n";
$message .= "Email: " . ($email ?: 'No proporcionado') . "\n";
$message .= "Asunto: $subject\n\n";
$message .= "Descripción:\n$description\n\n";
$message .= "Accede al panel administrativo para gestionarla.";

$headers = "From: no-reply@tuempresa.com\r\nContent-Type: text/plain; charset=utf-8";
mail($to, $subjectMail, $message, $headers);

// Si el usuario puso email, le enviamos confirmación
if ($email) {
    $msgUser = "Tu denuncia ha sido registrada.\n\nCódigo de seguimiento: $code\nGuárdalo para consultar el estado.\n\nGracias por confiar en nosotros.";
    mail($email, "Confirmación de denuncia - Código: $code", $msgUser, $headers);
}
?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Denuncia registrada</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="card shadow text-center p-5">
        <h2 class="text-success">¡Denuncia registrada correctamente!</h2>
        <div class="alert alert-info mt-4">
            <h4>CÓDIGO DE SEGUIMIENTO:</h4>
            <h2 class="text-primary"><?= htmlspecialchars($code) ?></h2>
            <p class="mt-3"><strong>Guarda este código</strong>. Es la única forma de seguir tu denuncia (incluso si eres anónimo).</p>
        </div>
        
        <?php if ($email): ?>
            <p>Te hemos enviado una copia al email <strong><?= htmlspecialchars($email) ?></strong></p>
        <?php endif; ?>
        
        <a href="track.php" class="btn btn-primary btn-lg mt-3">Ver estado de mi denuncia</a>
        <a href="index.php" class="btn btn-outline-secondary mt-3">Volver al inicio</a>
    </div>
</div>
</body>
</html>



PHP
<?php
// track.php
require_once 'config.php';
$complaint = null;
$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['code'])) {
    $code = strtoupper(trim($_POST['code']));
    
    $stmt = $pdo->prepare("SELECT * FROM complaints WHERE code = ?");
    $stmt->execute([$code]);
    $complaint = $stmt->fetch();
    
    if (!$complaint) {
        $message = "No se encontró ninguna denuncia con ese código.";
    }
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Seguimiento de denuncia</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="card shadow">
        <div class="card-header bg-dark text-white">
            <h4>Seguimiento de denuncia</h4>
        </div>
        <div class="card-body">
            <?php if (!$complaint): ?>
            <form method="POST">
                <div class="input-group mb-3">
                    <input type="text" name="code" class="form-control form-control-lg" placeholder="Introduce tu código (ej: DEN-ABC12345)" required>
                    <button class="btn btn-primary btn-lg">Consultar</button>
                </div>
            </form>
            <?php if ($message): ?>
                <div class="alert alert-danger"><?= htmlspecialchars($message) ?></div>
            <?php endif; ?>
            <?php else: ?>
                <div class="alert alert-info">
                    <strong>Código:</strong> <?= htmlspecialchars($complaint['code']) ?><br>
                    <strong>Estado actual:</strong> 
                    <span class="badge bg-<?= $complaint['status'] === 'Resuelta' ? 'success' : ($complaint['status'] === 'En proceso' ? 'warning' : 'secondary') ?>">
                        <?= htmlspecialchars($complaint['status']) ?>
                    </span>
                </div>
                
                <h5><?= htmlspecialchars($complaint['subject']) ?></h5>
                <p><?= nl2br(htmlspecialchars($complaint['description'])) ?></p>
                
                <?php if ($complaint['admin_notes']): ?>
                    <div class="border-start border-3 border-primary ps-3">
                        <strong>Notas de la empresa:</strong><br>
                        <?= nl2br(htmlspecialchars($complaint['admin_notes'])) ?>
                    </div>
                <?php endif; ?>
                
                <hr>
                <a href="track.php" class="btn btn-outline-secondary">Consultar otro código</a>
            <?php endif; ?>
        </div>
    </div>
</div>
</body>
</html>



Crea una carpeta admin y dentro pon estos dos archivos:
admin/login.php

<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($_POST['user'] === 'admin' && $_POST['pass'] === 'admin123') {  // CAMBIA LA CONTRASEÑA
        $_SESSION['admin'] = true;
        header('Location: index.php');
        exit;
    } else {
        $error = "Credenciales incorrectas";
    }
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Login Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-dark">
<div class="container mt-5">
    <div class="card mx-auto" style="max-width:400px;">
        <div class="card-body">
            <h3 class="text-center">Panel Empresa</h3>
            <?php if (isset($error)) echo "<div class='alert alert-danger'>$error</div>"; ?>
            <form method="POST">
                <input type="text" name="user" class="form-control mb-3" placeholder="Usuario" value="admin" required>
                <input type="password" name="pass" class="form-control mb-3" placeholder="Contraseña" required>
                <button class="btn btn-primary w-100">Entrar</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>


admin/index.php

<?php
session_start();
if (!isset($_SESSION['admin'])) {
    header('Location: login.php');
    exit;
}
require_once '../config.php';

// Actualizar estado y notas
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
    $stmt = $pdo->prepare("UPDATE complaints SET status = ?, admin_notes = ? WHERE id = ?");
    $stmt->execute([$_POST['status'], $_POST['admin_notes'], $_POST['id']]);
    $success = "Denuncia actualizada";
}

// Listado
$stmt = $pdo->query("SELECT * FROM complaints ORDER BY created_at DESC");
$complaints = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Panel Administrativo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h2>Panel de Reclamaciones <a href="../index.php" class="btn btn-sm btn-outline-secondary">Volver al sitio público</a></h2>
    
    <?php if (isset($success)) echo "<div class='alert alert-success'>$success</div>"; ?>
    
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Código</th>
                <th>Fecha</th>
                <th>Nombre</th>
                <th>Asunto</th>
                <th>Estado</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
        <?php foreach ($complaints as $c): ?>
            <tr>
                <td><strong><?= htmlspecialchars($c['code']) ?></strong></td>
                <td><?= $c['created_at'] ?></td>
                <td><?= htmlspecialchars($c['name'] ?? 'Anónimo') ?></td>
                <td><?= htmlspecialchars($c['subject']) ?></td>
                <td>
                    <span class="badge bg-<?= $c['status']==='Resuelta'?'success':($c['status']==='En proceso'?'warning':'secondary') ?>">
                        <?= $c['status'] ?>
                    </span>
                </td>
                <td>
                    <button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#modal<?= $c['id'] ?>">Gestionar</button>
                </td>
            </tr>
            
            <!-- Modal de edición -->
            <div class="modal fade" id="modal<?= $c['id'] ?>">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <form method="POST">
                            <div class="modal-header">
                                <h5>Gestión - <?= htmlspecialchars($c['code']) ?></h5>
                            </div>
                            <div class="modal-body">
                                <input type="hidden" name="id" value="<?= $c['id'] ?>">
                                <select name="status" class="form-select mb-3">
                                    <option value="Pendiente" <?= $c['status']=='Pendiente'?'selected':'' ?>>Pendiente</option>
                                    <option value="En proceso" <?= $c['status']=='En proceso'?'selected':'' ?>>En proceso</option>
                                    <option value="Resuelta" <?= $c['status']=='Resuelta'?'selected':'' ?>>Resuelta</option>
                                    <option value="Rechazada" <?= $c['status']=='Rechazada'?'selected':'' ?>>Rechazada</option>
                                </select>
                                <textarea name="admin_notes" class="form-control" rows="4" placeholder="Notas internas / respuesta al usuario"><?= htmlspecialchars($c['admin_notes'] ?? '') ?></textarea>
                            </div>
                            <div class="modal-footer">
                                <button type="submit" class="btn btn-success">Guardar cambios</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        <?php endforeach; ?>
        </tbody>
    </table>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>



Cómo instalar (en 2 minutos):

Crea la base de datos y ejecuta database.sql.
Sube todos los archivos a tu servidor (o XAMPP/WAMP).
Cambia en config.php la contraseña de MySQL y el email de la empresa.
En admin/login.php cambia la contraseña por una segura.
Accede a index.php para denuncias y a admin/login.php para la empresa.

¡Listo! El sistema es 100% funcional, admite anónimos, genera códigos, envía notificación a la empresa y permite seguimiento + evolución de estado.
Si quieres añadir adjuntos, historial completo de cambios o login más seguro (con password_hash), dime y te lo amplío en 5 minutos.


https://grok.com/c/fecc6f2e-83d1-4dfc-bc17-f65eedfeb7a2?rid=9c63480f-7ef2-4790-88f0-fc921891f707


C:\Users\CursoM\Documents\arturo.avisando.com\practicas\reclamaciones\admin\login.php
arturo.avisando.com\practicas\reclamaciones\admin\login.php
arturo.avisando.com\practicas\reclamaciones\index.php


alfonso y ciberweb69

42 es desde https://www.php.net/download-docs.php

45: el código DEN-X4YI8793 corresponde a mis pruebas en anónimo


Tiempo del trayecto: 01:12


49: si quiero me quedaría impedir que se pueda ir hacia atrás... porque lo hace
También hay que ver si actualizando sin más el panel admin recoge la incidencia... sí, lo hace (55)

Sí, perfecto, me deja ir sin problemas a la página de consultas https://arturo.avisando.com/practicas/reclamaciones/track.php


Neos txt

temp-mail, para correos temporales



Para comentar bloques de código en Visual Studio Code, el atajo correcto por defecto es Shift + Alt + A (en Windows/Linux) o Option + Command + A (en Mac), no Ctrl + Shift + A. Alternativamente, puedes usar Ctrl + K seguido de Ctrl + C para comentar líneas seleccionadas. 
YouTube
YouTube
 +1
Aquí tienes las soluciones detalladas:
Este video muestra cómo comentar y descomentar código rápidamente en Visual Studio Code:

Comentar bloque: Selecciona el código y presiona Shift + Alt + A (o Ctrl + K y luego Ctrl + C).
Descomentar: Selecciona el bloque comentado y presiona Shift + Alt + A de nuevo.
Verificar Atajos (Keybindings): Si sigue sin funcionar, presiona Ctrl + K, luego Ctrl + S, busca editor.action.blockComment y verifica qué atajo tiene asignado.
Conflictos: A veces, otras aplicaciones (como utilidades de tarjetas gráficas) roban el atajo. Intenta cambiarlo en la configuración de teclado de VS Code si es necesario. 
Stack Overflow en español
Stack Overflow en español
 +2
Asegúrate de tener seleccionado el texto antes de intentar comentarlo.


https://es.stackoverflow.com/questions/64854/c%C3%B3mo-comentar-las-l%C3%ADneas-seleccionadas-en-vscode

// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);




por qué los pantallazos se pegan en powerpoint reducidos

Los pantallazos o capturas de pantalla se pegan reducidos en PowerPoint principalmente porque el programa ajusta automáticamente el tamaño de la imagen para que quepa dentro de los márgenes de la diapositiva, independientemente de la resolución original de la pantalla capturada. 
Reddit
Reddit
 +1
Aquí están las razones detalladas y cómo evitarlo:
Por qué sucede:
Ajuste automático a la diapositiva: PowerPoint intenta que la imagen sea visible en su totalidad al pegarla. Si la resolución de tu pantalla es mayor que la resolución de la diapositiva, el programa reduce la imagen para evitar que se salga de los bordes.
Resolución de la imagen (DPI/PPP): Las capturas de pantalla a menudo se pegan con una configuración de 96 o 120 DPI, lo que las hace parecer más pequeñas cuando se colocan en un lienzo de mayor resolución.
Compresión automática: PowerPoint a veces aplica una compresión predeterminada para evitar que el archivo sea demasiado pesado. 
YouTube
YouTube
 +2



Cómo evitar que los pantallazos se reduzcan:
Insertar en lugar de pegar: En lugar de Ctrl+V, ve a la pestaña Insertar > Imágenes > Este dispositivo y selecciona la imagen. Esto suele mantener el tamaño original con mayor precisión.
Desactivar la compresión: Ve a Archivo > Opciones > Avanzadas. En la sección "Tamaño y calidad de la imagen", activa la opción "No comprimir las imágenes del archivo".
Configurar la resolución predeterminada: En el mismo menú (Archivo > Opciones > Avanzadas), puedes establecer la resolución predeterminada a "Alta fidelidad".
Ajustar manualmente al 100%: Tras pegar la imagen, haz clic derecho sobre ella, selecciona "Tamaño y posición" y asegúrate de que la escala esté al 100% (y activa "Bloquear relación de aspecto").
Utilizar la herramienta de recorte de pantalla nativa: Dentro de PowerPoint, usa Insertar > Captura > Recorte de pantalla, ya que esta herramienta gestiona mejor el tamaño que copiar desde el portapapeles de Windows. 
Microsoft
Microsoft
 +4


66, 67: pues en mi caso no era eso... tendré que revisar las opciones avanzadas de mi powerpoint

76: vista cuando no escribo notas de la empresa

77: voy a probar las denuncias no anónimas

79: perfecto, me da código también con la denuncia no anónima (en este caso DEN-UZP531VR)

82 viene de https://www.php.net/manual/es/function.mail.php

100: cuidado con los archivos htaccess


*** Día 17/03/2026

Sobre el código de ayer:

¿Puedes convertir este código a Javascript?


<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canal de Reclamaciones - MiEmpresa (JS Edition)</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body { background: #f8f9fa; }
        .hero { background: linear-gradient(135deg, #0d6efd, #6610f2); color: white; padding: 80px 0; }
        .section { display: none; }
        .section.active { display: block; }
        .badge-pendiente { background-color: #6c757d; }
        .badge-enproceso { background-color: #ffc107; color: black; }
        .badge-resuelta { background-color: #198754; }
        .badge-rechazada { background-color: #dc3545; }
    </style>
</head>
<body>
    <!-- NAVEGACIÓN -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="#">Canal de Reclamaciones</a>
            <div class="navbar-nav">
                <button onclick="showSection('public')" class="btn btn-outline-light me-2">Presentar Denuncia</button>
                <button onclick="showSection('track')" class="btn btn-outline-light me-2">Seguimiento</button>
                <button onclick="showAdminLogin()" class="btn btn-warning">Panel Empresa</button>
            </div>
        </div>
    </nav>

    <!-- HERO + FORMULARIO PÚBLICO -->
    <div id="public" class="section active">
        <div class="hero text-center">
            <div class="container">
                <h1 class="display-4">Canal de Reclamaciones</h1>
                <p class="lead">Presenta tu denuncia o queja. Se admiten denuncias anónimas.</p>
                <a href="#formulario" class="btn btn-light btn-lg">Presentar denuncia ahora</a>
            </div>
        </div>

        <div class="container mt-5" id="formulario">
            <div class="row justify-content-center">
                <div class="col-lg-8">
                    <div class="card shadow">
                        <div class="card-header bg-primary text-white">
                            <h4 class="mb-0">Formulario de denuncia</h4>
                        </div>
                        <div class="card-body">
                            <form id="denunciaForm">
                                <div class="mb-3">
                                    <label class="form-label">Nombre (opcional - si dejas en blanco será anónimo)</label>
                                    <input type="text" id="name" class="form-control" placeholder="Juan Pérez">
                                </div>
                                <div class="mb-3">
                                    <label class="form-label">Email (opcional - solo si quieres recibir confirmación)</label>
                                    <input type="email" id="email" class="form-control" placeholder="tu@email.com">
                                </div>
                                <div class="mb-3">
                                    <label class="form-label">Asunto *</label>
                                    <input type="text" id="subject" class="form-control" required>
                                </div>
                                <div class="mb-3">
                                    <label class="form-label">Descripción de la denuncia *</label>
                                    <textarea id="description" class="form-control" rows="6" required></textarea>
                                </div>
                                <button type="submit" class="btn btn-primary btn-lg w-100">Enviar denuncia</button>
                            </form>
                        </div>
                        <div class="card-footer text-center">
                            <a href="#" onclick="showSection('track')" class="btn btn-outline-secondary">Seguimiento de mi denuncia</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- PANTALLA DE ÉXITO -->
    <div id="success" class="section">
        <div class="container mt-5">
            <div class="card shadow text-center p-5">
                <h2 class="text-success">¡Denuncia registrada correctamente!</h2>
                <div class="alert alert-info mt-4">
                    <h4>CÓDIGO DE SEGUIMIENTO:</h4>
                    <h2 class="text-primary" id="success-code"></h2>
                    <p class="mt-3"><strong>Guarda este código</strong>. Es la única forma de seguir tu denuncia (incluso si eres anónimo).</p>
                </div>
                <p id="success-email-info" class="d-none">Te hemos enviado una copia al email proporcionado.</p>
                <div class="mt-4">
                    <button onclick="showSection('track')" class="btn btn-primary btn-lg me-3">Ver estado de mi denuncia</button>
                    <button onclick="showSection('public')" class="btn btn-outline-secondary btn-lg">Nueva denuncia</button>
                </div>
            </div>
        </div>
    </div>

    <!-- PANTALLA DE SEGUIMIENTO -->
    <div id="track" class="section">
        <div class="container mt-5">
            <div class="card shadow">
                <div class="card-header bg-dark text-white">
                    <h4>Seguimiento de denuncia</h4>
                </div>
                <div class="card-body">
                    <form id="trackForm" class="mb-4">
                        <div class="input-group">
                            <input type="text" id="track-code" class="form-control form-control-lg" placeholder="Introduce tu código (ej: DEN-ABC12345)" required>
                            <button class="btn btn-primary btn-lg">Consultar</button>
                        </div>
                    </form>

                    <div id="track-result" class="d-none">
                        <div class="alert alert-info">
                            <strong>Código:</strong> <span id="result-code"></span><br>
                            <strong>Estado actual:</strong> 
                            <span id="result-status" class="badge"></span>
                        </div>
                        <h5 id="result-subject"></h5>
                        <p id="result-description"></p>
                        <div id="result-notes" class="border-start border-3 border-primary ps-3 d-none">
                            <strong>Notas de la empresa:</strong><br>
                            <span id="result-notes-text"></span>
                        </div>
                        <hr>
                        <button onclick="resetTrack()" class="btn btn-outline-secondary">Consultar otro código</button>
                    </div>

                    <div id="track-error" class="alert alert-danger d-none"></div>
                </div>
            </div>
        </div>
    </div>

    <!-- PANEL ADMIN (solo visible después de login) -->
    <div id="admin" class="section">
        <div class="container mt-4">
            <div class="d-flex justify-content-between align-items-center mb-3">
                <h2>Panel de Reclamaciones <span class="badge bg-success">ADMIN</span></h2>
                <div>
                    <button onclick="logoutAdmin()" class="btn btn-danger btn-sm">Cerrar sesión</button>
                    <button onclick="showSection('public')" class="btn btn-outline-secondary btn-sm">Volver al sitio público</button>
                </div>
            </div>

            <table class="table table-striped" id="admin-table">
                <thead class="table-dark">
                    <tr>
                        <th>Código</th>
                        <th>Fecha</th>
                        <th>Nombre</th>
                        <th>Asunto</th>
                        <th>Estado</th>
                        <th>Acciones</th>
                    </tr>
                </thead>
                <tbody></tbody>
            </table>
        </div>
    </div>

    <!-- MODAL LOGIN ADMIN -->
    <div class="modal fade" id="adminLoginModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Acceso Panel Empresa</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div id="login-error" class="alert alert-danger d-none"></div>
                    <form id="adminLoginForm">
                        <div class="mb-3">
                            <input type="text" id="admin-user" class="form-control" placeholder="Usuario" value="admin" required>
                        </div>
                        <div class="mb-3">
                            <input type="password" id="admin-pass" class="form-control" placeholder="Contraseña" required>
                        </div>
                        <button type="submit" class="btn btn-primary w-100">Entrar</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <!-- MODAL GESTIÓN ADMIN -->
    <div class="modal fade" id="editModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Gestión - <span id="modal-code"></span></h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="editForm">
                        <input type="hidden" id="modal-id">
                        <div class="mb-3">
                            <label class="form-label">Estado</label>
                            <select id="modal-status" class="form-select">
                                <option value="Pendiente">Pendiente</option>
                                <option value="En proceso">En proceso</option>
                                <option value="Resuelta">Resuelta</option>
                                <option value="Rechazada">Rechazada</option>
                            </select>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Notas internas / respuesta al usuario</label>
                            <textarea id="modal-notes" class="form-control" rows="5"></textarea>
                        </div>
                        <button type="submit" class="btn btn-success w-100">Guardar cambios</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        // ==================== BASE DE DATOS SIMULADA (localStorage) ====================
        let complaints = JSON.parse(localStorage.getItem('complaints')) || [];
        let isAdminLogged = false;

        function saveComplaints() {
            localStorage.setItem('complaints', JSON.stringify(complaints));
        }

        function generateCode() {
            const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
            let code = '';
            for (let i = 0; i < 8; i++) {
                code += chars[Math.floor(Math.random() * chars.length)];
            }
            return 'DEN-' + code;
        }

        // ==================== CAMBIAR SECCIÓN ====================
        function showSection(sectionId) {
            document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
            const section = document.getElementById(sectionId);
            if (section) section.classList.add('active');
            
            if (sectionId === 'admin' && !isAdminLogged) {
                showAdminLogin();
            }
            if (sectionId === 'track') {
                resetTrack();
            }
        }

        // ==================== FORMULARIO PÚBLICO ====================
        document.getElementById('denunciaForm').addEventListener('submit', function(e) {
            e.preventDefault();
            
            const name = document.getElementById('name').value.trim();
            const email = document.getElementById('email').value.trim();
            const subject = document.getElementById('subject').value.trim();
            const description = document.getElementById('description').value.trim();

            if (!subject || !description) {
                alert('Asunto y descripción son obligatorios.');
                return;
            }

            const code = generateCode();
            const newComplaint = {
                id: Date.now(),
                code: code,
                name: name || null,
                email: email || null,
                subject: subject,
                description: description,
                status: 'Pendiente',
                admin_notes: '',
                created_at: new Date().toISOString(),
                updated_at: new Date().toISOString()
            };

            complaints.push(newComplaint);
            saveComplaints();

            // Simular email a la empresa
            console.log(`📧 NOTIFICACIÓN ENVIADA A LA EMPRESA:\nCódigo: ${code}\nNombre: ${name || 'Anónimo'}\nAsunto: ${subject}`);
            alert('✅ Denuncia enviada correctamente.\n\n(En un entorno real se enviaría email a atención@tuempresa.com)');

            // Simular confirmación al usuario
            if (email) {
                console.log(`📧 CONFIRMACIÓN ENVIADA A: ${email}\nCódigo: ${code}`);
                document.getElementById('success-email-info').classList.remove('d-none');
            } else {
                document.getElementById('success-email-info').classList.add('d-none');
            }

            // Mostrar pantalla de éxito
            document.getElementById('success-code').textContent = code;
            showSection('success');

            // Limpiar formulario
            document.getElementById('denunciaForm').reset();
        });

        // ==================== SEGUIMIENTO ====================
        document.getElementById('trackForm').addEventListener('submit', function(e) {
            e.preventDefault();
            const codeInput = document.getElementById('track-code').value.trim().toUpperCase();
            const complaint = complaints.find(c => c.code === codeInput);

            document.getElementById('track-result').classList.add('d-none');
            document.getElementById('track-error').classList.add('d-none');

            if (!complaint) {
                document.getElementById('track-error').textContent = 'No se encontró ninguna denuncia con ese código.';
                document.getElementById('track-error').classList.remove('d-none');
                return;
            }

            // Mostrar resultados
            document.getElementById('result-code').textContent = complaint.code;
            
            const statusEl = document.getElementById('result-status');
            statusEl.textContent = complaint.status;
            statusEl.className = 'badge ' + 
                (complaint.status === 'Resuelta' ? 'bg-success' : 
                 complaint.status === 'En proceso' ? 'badge-enproceso' : 
                 complaint.status === 'Rechazada' ? 'badge-rechazada' : 'badge-pendiente');

            document.getElementById('result-subject').textContent = complaint.subject;
            document.getElementById('result-description').textContent = complaint.description;

            const notesDiv = document.getElementById('result-notes');
            if (complaint.admin_notes && complaint.admin_notes.trim() !== '') {
                document.getElementById('result-notes-text').innerHTML = complaint.admin_notes.replace(/\n/g, '<br>');
                notesDiv.classList.remove('d-none');
            } else {
                notesDiv.classList.add('d-none');
            }

            document.getElementById('track-result').classList.remove('d-none');
        });

        function resetTrack() {
            document.getElementById('track-result').classList.add('d-none');
            document.getElementById('track-error').classList.add('d-none');
            document.getElementById('track-code').value = '';
        }

        // ==================== ADMIN LOGIN ====================
        let bootstrapModalLogin, bootstrapModalEdit;

        function showAdminLogin() {
            if (isAdminLogged) {
                showSection('admin');
                return;
            }
            document.getElementById('login-error').classList.add('d-none');
            bootstrapModalLogin = new bootstrap.Modal(document.getElementById('adminLoginModal'));
            bootstrapModalLogin.show();
        }

        document.getElementById('adminLoginForm').addEventListener('submit', function(e) {
            e.preventDefault();
            const user = document.getElementById('admin-user').value.trim();
            const pass = document.getElementById('admin-pass').value.trim();

            if (user === 'admin' && pass === 'admin123') {  // ← CAMBIA ESTA CONTRASEÑA
                isAdminLogged = true;
                bootstrapModalLogin.hide();
                showSection('admin');
                renderAdminTable();
            } else {
                const errorEl = document.getElementById('login-error');
                errorEl.textContent = 'Credenciales incorrectas';
                errorEl.classList.remove('d-none');
            }
        });

        function logoutAdmin() {
            isAdminLogged = false;
            showSection('public');
        }

        // ==================== PANEL ADMIN ====================
        function renderAdminTable() {
            const tbody = document.querySelector('#admin-table tbody');
            tbody.innerHTML = '';

            complaints.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));

            complaints.forEach(c => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td><strong>${c.code}</strong></td>
                    <td>${new Date(c.created_at).toLocaleDateString('es-ES')}</td>
                    <td>${c.name || 'Anónimo'}</td>
                    <td>${c.subject}</td>
                    <td>
                        <span class="badge ${c.status === 'Resuelta' ? 'bg-success' : 
                                            c.status === 'En proceso' ? 'badge-enproceso' : 
                                            c.status === 'Rechazada' ? 'badge-rechazada' : 'badge-pendiente'}">
                            ${c.status}
                        </span>
                    </td>
                    <td>
                        <button class="btn btn-sm btn-primary" onclick="openEditModal(${c.id})">Gestionar</button>
                    </td>
                `;
                tbody.appendChild(row);
            });
        }

        // ==================== MODAL EDICIÓN ====================
        function openEditModal(id) {
            const complaint = complaints.find(c => c.id === id);
            if (!complaint) return;

            document.getElementById('modal-id').value = complaint.id;
            document.getElementById('modal-code').textContent = complaint.code;
            document.getElementById('modal-status').value = complaint.status;
            document.getElementById('modal-notes').value = complaint.admin_notes || '';

            bootstrapModalEdit = new bootstrap.Modal(document.getElementById('editModal'));
            bootstrapModalEdit.show();
        }

        document.getElementById('editForm').addEventListener('submit', function(e) {
            e.preventDefault();
            
            const id = parseInt(document.getElementById('modal-id').value);
            const status = document.getElementById('modal-status').value;
            const notes = document.getElementById('modal-notes').value.trim();

            const complaint = complaints.find(c => c.id === id);
            if (complaint) {
                complaint.status = status;
                complaint.admin_notes = notes;
                complaint.updated_at = new Date().toISOString();
                saveComplaints();
                renderAdminTable();
            }

            bootstrapModalEdit.hide();
            alert('✅ Cambios guardados correctamente.');
        });

        // ==================== INICIALIZACIÓN ====================
        window.onload = function() {
            // Mostrar sección pública por defecto
            showSection('public');

            // Si hay datos previos, los mostramos (útil para pruebas)
            if (complaints.length > 0) {
                console.log(`📊 ${complaints.length} denuncias cargadas de localStorage`);
            }
        };

        // ==================== NOTA FINAL ====================
        console.log('%c✅ Sistema convertido a JavaScript 100% funcional (SPA + localStorage)\n' +
                    '• Datos se guardan en tu navegador\n' +
                    '• No necesita servidor ni MySQL\n' +
                    '• Emails simulados (mira la consola)\n' +
                    '• Usuario admin / contraseña: admin123 (cámbiala en el código)\n' +
                    '¡Listo para usar! Copia todo este código en un archivo index.html y ábrelo.', 
                    'color: green; font-weight: bold;');
    </script>
</body>
</html>



sqlite que es y para que sirve


SQLite es un motor de base de datos relacional ligero, de código abierto y embebido (integrado) que no requiere servidor ni configuración compleja, almacenando toda la información en un solo archivo físico. Sirve para gestionar datos de forma eficiente en aplicaciones móviles (Android/iOS), navegadores, dispositivos IoT y software de escritorio. 
| SG Buzz
| SG Buzz
 +4
Este video ofrece una introducción para principiantes sobre qué es SQLite:



Características Principales:
Embebida: Se integra directamente dentro de la aplicación, sin procesos cliente-servidor.
Basada en archivos: Toda la base de datos (tablas, índices, vistas) es un único archivo en el disco.
SQL Estándar: Utiliza el lenguaje SQL para realizar consultas.
Ligera y rápida: Muy rápida para lectura y escritura de datos, ideal para almacenamiento local. 
| SG Buzz
| SG Buzz
 +4
¿Para qué sirve?
Almacenamiento en Apps: Es el estándar en Android y iOS para guardar datos de usuario.
Aplicaciones de escritorio: Programas como Skype, navegadores web (Chrome, Firefox) la usan para guardar historial o configuraciones.
Prototipado: Útil para proyectos pequeños donde una base de datos pesada como MySQL es innecesaria.
Dispositivos Embebidos: Ideal para IoT (Internet de las cosas), cámaras, televisores y coches. 
| SG Buzz
| SG Buzz
 +4
Ventajas y Desventajas:
✅ Fácil de usar: No requiere instalación, ni administración, ni configuración de usuarios.
✅ Multiplataforma: El archivo es compatible entre Windows, Linux y Mac.
❌ No ideal para múltiples usuarios: Está diseñada para uso local, no funciona bien con muchos usuarios escribiendo simultáneamente.
❌ No escalable: No diseñada para bases de datos masivas (aunque soporta hasta 128 Terabytes en la versión 3, no es eficiente para ello). 
YouTube
YouTube
 +4
DB Browser for SQLite es una herramienta gráfica popular para gestionar estos archivos. 
KeepCoding
KeepCoding
 +2



32: ¿Y puedes hacerlo en Python?, quise preguntar... pero ya no me quiere.
Al menos lo tengo en JavaScript, desde https://grok.com/c/21dd9f73-4f2c-41d8-a317-eea387b5c882

¿Puedes convertirme este código a Python?... y lo hace pero "interrumpido"

¡Claro! He convertido todo el sistema de PHP a Python usando Flask (el framework más sencillo y parecido a PHP puro).
Mantengo exactamente la misma funcionalidad:

Denuncias anónimas
Código único (DEN-XXXXXXX)
Envío de emails (a la empresa + confirmación al usuario)
Seguimiento público con el código
Panel administrativo con login simple + edición de estado y notas
Diseño Bootstrap idéntico

Ventajas de esta versión:

Solo necesitas pip install flask
Base de datos SQLite (se crea automáticamente, sin instalar MySQL)
Mucho más fácil de desplegar (XAMPP, Railway, PythonAnywhere, etc.)
Código limpio y moderno

Cómo instalar y ejecutar (2 minutos)

Crea una carpeta nueva (ej: reclamaciones-python)
Dentro crea la carpeta templates (y otra templates/admin)
Guarda los archivos que te doy abajo
Abre terminal y ejecuta:
Bash
pip install flask
python app.py

Abre en el navegador: http://127.0.0.1:5000

¡Listo! Funciona al 100 %.

1. app.py (el archivo principal)


from flask import Flask, render_template, request, redirect, url_for, session, flash
import sqlite3
import string
import random
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'clave_super_secreta_cambia_esta'  # ¡CÁMBIALA!

EMAIL_EMPRESA = 'atencion@tuempresa.com'  # ← Cambia por tu email real

def get_db():
    conn = sqlite3.connect('reclamaciones.db')
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    conn = get_db()
    conn.execute('''
        CREATE TABLE IF NOT EXISTS complaints (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            code TEXT UNIQUE NOT NULL,
            name TEXT,
            email TEXT,
            subject TEXT NOT NULL,
            description TEXT NOT NULL,
            status TEXT DEFAULT 'Pendiente',
            admin_notes TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

# Crear BD al iniciar
with app.app_context():
    init_db()

# ===================== FUNCIÓN EMAIL (demo) =====================
def enviar_email(destino, asunto, mensaje):
    print(f"\n=== EMAIL ENVIADO ===\nPara: {destino}\nAsunto: {asunto}\nMensaje:\n{mensaje}\n====================\n")
    # Si quieres email real, descomenta y configura smtplib (Gmail, etc.)

# ===================== RUTAS =====================

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        nombre = request.form.get('name', '').strip()
        email = request.form.get('email', '').strip()
        asunto = request.form.get('subject', '').strip()
        descripcion = request.form.get('description', '').strip()

        if not asunto or not descripcion:
            flash("Asunto y descripción son obligatorios.")
            return redirect(url_for('index'))

        # Código único
        code = 'DEN-' + ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))

        # Guardar en BD
        conn = get_db()
        conn.execute(
            "INSERT INTO complaints (code, name, email, subject, description) VALUES (?, ?, ?, ?, ?)",
            (code, nombre or None, email or None, asunto, descripcion)
        )
        conn.commit()
        conn.close()

        # Email a la empresa
        msg_empresa = f"Nueva denuncia recibida\nCódigo: {code}\nNombre: {nombre or 'Anónimo'}\nEmail: {email or 'No proporcionado'}\nAsunto: {asunto}\n\nDescripción:\n{descripcion}"
        enviar_email(EMAIL_EMPRESA, f"Nueva denuncia - Código: {code}", msg_empresa)

        # Confirmación al usuario (si dio email)
        if email:
            msg_user = f"Tu denuncia ha sido registrada correctamente.\n\nCódigo de seguimiento: {code}\nGuárdalo para consultar el estado.\n\nGracias por confiar en nosotros."
            enviar_email(email, f"Confirmación de denuncia - Código: {code}", msg_user)

        return render_template('success.html', code=code, email=email)

    return render_template('index.html')


@app.route('/track', methods=['GET', 'POST'])
def track():
    complaint = None
    mensaje = ''
    if request.method == 'POST':
        code = request.form.get('code', '').strip().upper()
        conn = get_db()
        row = conn.execute("SELECT * FROM complaints WHERE code = ?", (code,)).fetchone()
        conn.close()
        if row:
            complaint = dict(row)
        else:
            mensaje = "No se encontró ninguna denuncia con ese código."
    return render_template('track.html', complaint=complaint, mensaje=mensaje)


# ===================== PANEL ADMIN =====================
@app.route('/admin/login', methods=['GET', 'POST'])
def admin_login():
    if request.method == 'POST':
        if request.form['user'] == 'admin' and request.form['pass'] == 'admin123':  # ← CAMBIA LA CONTRASEÑA
            session['admin'] = True
            return redirect(url_for('admin_index'))
        else:
            flash("Credenciales incorrectas")
    return render_template('admin/login.html')


@app.route('/admin/')
def admin_index():
    if not session.get('admin'):
        return redirect(url_for('admin_login'))
    conn = get_db()
    complaints = conn.execute("SELECT * FROM complaints ORDER BY created_at DESC").fetchall()
    conn.close()
    return render_template('admin/index.html', complaints=[dict(c) for c in complaints])


@app.route('/admin/update', methods=['POST'])
def admin_update():
    if not session.get('admin'):
        return redirect(url_for('admin_login'))
    conn = get_db()
    conn.execute(
        "UPDATE complaints SET status = ?, admin_notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
        (request.form['status'], request.form['admin_notes'], request.form['id'])
    )
    conn.commit()
    conn.close()
    flash("Denuncia actualizada correctamente")
    return redirect(url_for('admin_index'))


@app.route('/admin/logout')
def admin_logout():
    session.pop('admin', None)
    return redirect(url_for('admin_login'))


if __name__ == '__main__':
    app.run(debug=True)


2. Carpeta templates/
templates/index.html


<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canal de Reclamaciones - MiEmpresa</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>body{background:#f8f9fa;}.hero{background:linear-gradient(135deg,#0d6efd,#6610f2);color:white;padding:80px 0;}</style>
</head>
<body>
<div class="hero text-center">
    <div class="container">
        <h1 class="display-4">Canal de Reclamaciones</h1>
        <p class="lead">Presenta tu denuncia o queja. Se admiten denuncias anónimas.</p>
        <a href="#formulario" class="btn btn-light btn-lg">Presentar denuncia ahora</a>
    </div>
</div>

<div class="container mt-5" id="formulario">
    <div class="row justify-content-center">
        <div class="col-lg-8">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h4 class="mb-0">Formulario de denuncia</h4>
                </div>
                <div class="card-body">
                    <form method="POST">
                        <div class="mb-3">
                            <label class="form-label">Nombre (opcional - anónimo si dejas en blanco)</label>
                            <input type="text" name="name" class="form-control" placeholder="Juan Pérez">
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Email (opcional - solo para confirmación)</label>
                            <input type="email" name="email" class="form-control" placeholder="tu@email.com">
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Asunto *</label>
                            <input type="text" name="subject" class="form-control" required>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Descripción de la denuncia *</label>
                            <textarea name="description" class="form-control" rows="6" required></textarea>
                        </div>
                        <button type="submit" class="btn btn-primary btn-lg w-100">Enviar denuncia</button>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <a href="/track" class="btn btn-outline-secondary">Seguimiento de mi denuncia</a>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>



templates/success.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Denuncia registrada</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="card shadow text-center p-5">
        <h2 class="text-success">¡Denuncia registrada correctamente!</h2>
        <div class="alert alert-info mt-4">
            <h4>CÓDIGO DE SEGUIMIENTO:</h4>
            <h2 class="text-primary">{{ code }}</h2>
            <p><strong>Guarda este código</strong>. Es la única forma de seguir tu denuncia (incluso si eres anónimo).</p>
        </div>
        {% if email %}
            <p>Te hemos enviado una copia al email <strong>{{ email }}</strong></p>
        {% endif %}
        <a href="/track" class="btn btn-primary btn-lg mt-3">Ver estado de mi denuncia</a>
        <a href="/" class="btn btn-outline-secondary mt-3">Volver al inicio</a>
    </div>
</div>
</body>
</html>


templates/track.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Seguimiento de denuncia</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="card shadow">
        <div class="card-header bg-dark text-white">
            <h4>Seguimiento de denuncia</h4>
        </div>
        <div class="card-body">
            {% if not complaint %}
                <form method="POST">
                    <div class="input-group mb-3">
                        <input type="text" name="code" class="form-control form-control-lg" placeholder="Introduce tu código (ej: DEN-ABC12345)" required>
                        <button class="btn btn-primary btn-lg">Consultar</button>
                    </div>
                </form>
                {% if mensaje %}
                    <div class="alert alert-danger">{{ mensaje }}</div>
                {% endif %}
            {% else %}
                <div class="alert alert-info">
                    <strong>Código:</strong> {{ complaint.code }}<br>
                    <strong>Estado actual:</strong>
                    <span class="badge bg-{{ 'success' if complaint.status == 'Resuelta' else 'warning' if complaint.status == 'En proceso' else 'secondary' }}">
                        {{ complaint.status }}
                    </span>
                </div>
                <h5>{{ complaint.subject }}</h5>
                <p>{{ complaint.description | replace('\n', '<br>') | safe }}</p>
                {% if complaint.admin_notes %}
                    <div class="border-start border-3 border-primary ps-3">
                        <strong>Notas de la empresa:</strong><br>
                        {{ complaint.admin_notes | replace('\n', '<br>') | safe }}
                    </div>
                {% endif %}
                <hr>
                <a href="/track" class="btn btn-outline-secondary">Consultar otro código</a>
            {% endif %}
        </div>
    </div>
</div>
</body>
</html>


templates/admin/login.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Login Admin</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-dark">
<div class="container mt-5">
    <div class="card mx-auto" style="max-width:400px;">
        <div class="card-body">
            <h3 class="text-center text-white">Panel Empresa</h3>
            {% if get_flashed_messages() %}
                <div class="alert alert-danger">{{ get_flashed_messages()[0] }}</div>
            {% endif %}
            <form method="POST">
                <input type="text" name="user" class="form-control mb-3" value="admin" required>
                <input type="password" name="pass" class="form-control mb-3" required>
                <button class="btn btn-primary w-100">Entrar</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>



templates/admin/index.html (el más largo, pero completo)

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Panel Administrativo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h2>Panel de Reclamaciones</h2>
    <a href="/" class="btn btn-sm btn-outline-secondary">Volver al sitio público</a>
    <a href="/admin/logout" class="btn btn-sm btn-danger float-end">Cerrar sesión</a>
</div>

{% with messages = get_flashed_messages() %}
    {% if messages %}
        <div class="container"><div class="alert alert-success">{{ messages[0] }}</div></div>
    {% endif %}
{% endwith %}

<div class="container mt-4">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Código</th>
                <th>Fecha</th>
                <th>Nombre</th>
                <th>Asunto</th>
                <th>Estado</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
        {% for c in complaints %}
            <tr>
                <td><strong>{{ c.code }}</strong></td>
                <td>{{ c.created_at }}</td>
                <td>{{ c.name or 'Anónimo' }}</td>
                <td>{{ c.subject }}</td>
                <td>
                    <span class="badge bg-{{ 'success' if c.status == 'Resuelta' else 'warning' if c.status == 'En proceso' else 'secondary' }}">
                        {{ c.status }}
                    </span>
                </td>
                <td>
                   
Grok no pudo terminar de responder... se interrumpe aquí.
Por lo que veo me lleva a esa carpeta Templates mis archivos anteriores, relativamente transformados


https://grok.com/c/078927af-a4d3-40fd-bf4d-a215f4b2adac?rid=cbf6fed5-76c2-4d5b-b76b-75391db4d1c2

Reintento... pero 40 y ya no me contesta más


44: https://github.com/alphp/ModoX/blob/master/GW-BASIC%20Gu%C3%ADa%20del%20programador%20y%20manual%20de%20referencia.md

… "encoded binary format"

cómo ver código de archivo .BAS guardado en gwbasic de los años 80


Para ver el código de un archivo .BAS creado en GW-BASIC en los años 80, debes entender que la mayoría de esos archivos se guardaron en un formato binario tokenizado (comprimido), no como texto plano. Por lo tanto, no se pueden abrir directamente en el Bloc de notas.
Aquí tienes los métodos principales para ver el código, del más recomendado al más complejo:

1. El método recomendado: Usar DOSBox y GW-BASIC
Esta es la forma más fiable, ya que utiliza el intérprete original para "desencriptar" el archivo.
Descarga DOSBox: Instala DOSBox en tu computadora moderna.
Consigue GW-BASIC: Necesitas el archivo GWBASIC.EXE. Puedes encontrarlo en internet (Microsoft liberó el código fuente hace años).
Configura la carpeta: Pon tu archivo .BAS y GWBASIC.EXE en una carpeta (ej. C:\retro).
Ejecuta DOSBox: Monta la carpeta y corre GW-BASIC:
mount c c:\retro
c:
gwbasic
Carga y guarda en texto (ASCII): Una vez dentro de GW-BASIC, escribe:
LOAD "tuarchivo.bas" (Carga el archivo binario).
SAVE "nuevonombre.txt",a (La ,a es crucial: guarda en formato ASCII).
Visualiza: Sal de DOSBox (SYSTEM) y abre nuevonombre.txt en el Bloc de notas. 


2. Método rápido: Usar PC-BASIC
PC-BASIC es un emulador moderno de GW-BASIC que funciona en Windows, macOS y Linux sin necesidad de montar unidades.
Instala PC-BASIC.
Abre el programa y carga tu archivo: LOAD"tuarchivo.bas"
Guarda en texto: SAVE"codigo.txt",a
3. Solución si el archivo está "Protegido"
Si al intentar cargarlo o listar el código (LIST) obtienes errores o no ves nada, el archivo fue guardado con la opción ,P (protegido). La solución es utilizar un archivo binario para romper la protección: 
Crea un archivo llamado unprot.bas en DOSBox que contenga solo estos dos caracteres (usando DEBUG o un editor hexadecimal): 0xff y 0x1a.
En GW-BASIC, carga tu archivo protegido: LOAD"pro.bas"
Carga el archivo de desbloqueo: MERGE"unprot.bas"
Ahora podrás listar (LIST) y guardar el código. 


4. Alternativa: Convertidores en línea o herramientas de terceros
Existen descodificadores como gwbasic-decoder en GitHub que permiten convertir los archivos binarios a texto sin ejecutar el intérprete.
¿Por qué no funciona el Bloc de notas?
Si intentas abrirlo directamente, verás caracteres extraños. Esto se debe a que GW-BASIC reemplazaba palabras clave (PRINT, IF, THEN) por códigos binarios de un solo byte para ahorrar memoria en los años 80.


Y del grupo de referencias que toma la que más me gusta es esta:
https://stackoverflow.com/questions/41167778/how-do-i-view-the-source-code-of-a-gw-basic-bas-file#:~:text=You're%20not%20doing%20anything,255);%2030%20CLOSE%20%231

Aquí te dice:
Primera respuesta:
You're not doing anything wrong; the file was originally saved in GWBASIC with the ,P option. There is a 'hack' to unprotect it, described at https://groups.google.com/forum/#!topic/comp.os.msdos.misc/PA9sve0eKAk - basically, you create a file (call it UNPROT.BAS) containing only the characters 0xff 0x1a, then load the protected file, then load UNPROT.BAS, and you should then be able to list and save the program.

(A lo que otro apunta:
Thanks! This worked like a charm, but with one modification. The unprot.bas file had to have two characters: 0xff and 0x1a.)

Segunda respuesta:
If you can't LIST or EDIT a GW-BASIC .BAS file that you LOADed from disk, it means that the file was originally SAVEd in protected format via SAVE filespec, P.

The 1988 "Handbook of BASIC - third edition" by David I. Schneider describes it as follows:

A program that has been SAVEd in protected format can be unprotected with the following technique.

(a) Create a file called RECOVER.BAS with the following program.

 10 OPEN "RECOVER.BAS" FOR OUTPUT AS #1  
 20 PRINT #1, CHR$(255);  
 30 CLOSE #1  
(b) LOAD the protected program into memory.
(c) Enter LOAD "RECOVER.BAS"

The formerly protected program will now be in memory and can be LISTed or EDITed, and reSAVEd in an unprotected format. This technique appears to work with most versions of BASIC. I have used it successfully with IBM PC BASIC, Compaq BASIC, and several versions of GW-BASIC. LOADing the file RECOVER.BAS will also restore a program after a NEW command has been executed.


Chrome lo traduce así:

No estás haciendo nada malo; el archivo se guardó originalmente en GWBASIC con la ,Popción. Hay un truco para desprotegerlo, descrito en https://groups.google.com/forum/#!topic/comp.os.msdos.misc/PA9sve0eKAk . Básicamente, creas un archivo (llámalo UNPROT.BAS) que contiene solo los caracteres 0xff 0x1a, luego cargas el archivo protegido, después cargas UNPROT.BAS y, finalmente, deberías poder listar y guardar el programa.

¡Gracias! Funcionó a la perfección, pero con una modificación. El archivo unprot.bas debía contener dos caracteres: 0xff y 0x1a.



Si no puede LISTo EDITun archivo .BAS de GW-BASIC que haya LOADcopiado desde el disco, significa que el archivo fue SAVEcopiado originalmente en formato protegido a través de SAVE filespec, P.

El "Manual de BASIC - tercera edición" de 1988, de David I. Schneider, lo describe de la siguiente manera:

Un programa que se ha guardado en formato protegido se puede desproteger con la siguiente técnica.

(a) Cree un archivo llamado RECOVER.BAS con el siguiente programa.

 10 OPEN "RECOVER.BAS" FOR OUTPUT AS #1  
 20 PRINT #1, CHR$(255);  
 30 CLOSE #1  
(b) CARGAR el programa protegido en la memoria.
(c) Introducir CARGAR "RECOVER.BAS"

El programa, antes protegido, ahora estará en memoria y podrá listarse, editarse y guardarse de nuevo sin protección. Esta técnica parece funcionar con la mayoría de las versiones de BASIC. La he utilizado con éxito en IBM PC BASIC, Compaq BASIC y varias versiones de GW-BASIC. Cargar el archivo RECOVER.BAS también restaurará un programa después de ejecutar un comando NEW.


Pregunto "caracteres 0xff 0x1a"

Los valores hexadecimales 0xFF y 0x1A representan caracteres de control o códigos especiales en computación, a menudo asociados con el final de archivos o errores de codificación.
0x1A (Substitución/Fin de Archivo - EOF): Es el carácter ASCII 26, comúnmente conocido como SUB (Substitute). En sistemas DOS/Windows antiguos, este carácter marcaba el final de un archivo de texto (End of File - EOF). En contextos modernos, a menudo indica un error de datos o un byte que no tiene interpretación en la codificación actual.
0xFF (Byte de alto valor/MARCA): Es un valor de 8 bits (255 en decimal). Dependiendo del contexto, puede ser parte de una firma de archivo (como los primeros bytes de un archivo JPEG o UTF-16), o utilizado para indicar "ningún dato" en ciertas memorias. 
Stack Overflow
Stack Overflow
 +3
Contexto Técnico (Error 0x1A):
A veces, 0x1A aparece en mensajes de error relacionados con la BIOS/UEFI, indicando una "Violación de seguridad" (Security Violation) durante el arranque seguro (Secure Boot), especialmente al intentar arrancar Linux en hardware configurado para Windows. 
Reddit
Reddit
 +1


60: cómo escribir los caracteres 0xff 0x1a

Los caracteres 0xff y 0x1a son representaciones hexadecimales de valores de bytes, no caracteres alfanuméricos estándar. Escribirlos depende del contexto (un documento, código de programación o terminal). 
1. En Windows (Teclado numérico/Alt Codes):
0xff (255): Mantén presionada la tecla ALT y escribe 255 en el teclado numérico (Alt+255). Suele aparecer un espacio en blanco ( NBSP).
0x1a (26): Mantén presionada la tecla ALT y escribe 26 en el teclado numérico (Alt+26). Suele aparecer una flecha hacia la derecha (→), que representa el carácter de fin de archivo (EOF) en sistemas antiguos. 
HP
HP
 +1
2. En programación (C/C++, Arduino, Python):
Se utilizan secuencias de escape hexadecimales:
\xff
\x1a
Ejemplo en C/Arduino: Serial.write(0xFF); Serial.write(0x1A);. 
Reddit
Reddit
3. En editores hexadecimales:
Si estás editando un archivo binario, simplemente busca la opción de insertar bytes o escribe directamente FF y 1A en las celdas correspondientes.
Nota: El valor 0x1a a menudo es interpretado como un comando de "fin de archivo" (EOF) en terminales de Windows. 
accedaCRIS
accedaCRIS


https://cursos00.com/cursos-gratis-para-trabajadores/?utm_source=google&utm_medium=cpc&utm_campaign=trabajadores&utm_source=google&utm_medium=cpc&utm_campaign=trabajadores-presencial&utm_term=cursos%20subvencionados%20trabajadores%20madrid&utm_content=182470663591&gad_source=1&gad_campaignid=22560099023&gbraid=0AAAAADQZUNR2eJLQTMRZPlD9xsDAbCiQB&gclid=EAIaIQobChMIh-bCr_KmkwMVr2ZBAh3bBxdkEAAYASAAEgLsn_D_BwE
Y lo damos en el campusgp de Getafe



Dime el nombre de un juego arcade en el que eras una nave que cada P que iba cogiendo era una vida más. Disparabas a varios rivales: una serpiente verde larga, un dragón morado, asteroides...
Era en desplazamiento vertical todo el rato y en máquina de bar
Ibas claramente en una nave, no era criatura. El fondo era oscuro porque era el espacio
no es terra cresta

Dime el nombre de un juego arcade en el que tenías que escoger entre 5 planetas, y cada planeta era como un juego diferente. Un planeta tenía aspecto verde, el otro era como desértico...
De plataformas con personaje, años 80, en recreativa de bar, desplazamiento horizontal
El personaje era humano pero con traje de astronauta, y disparaba a los enemigos que iban saliendo
Podías elegir entre varios planetas al principio, y cuando acababas el juego de uno te volvía a sacar los otros para elegir uno


atomic runner chelnov arcade??

Nichibutsu arcade serpiente dragón


92: https://cursos00.com/cursos/servicios-en-la-nube-cloud-con-aws/?_gl=1*41abnh*_up*MQ..*_gs*MQ..&gclid=EAIaIQobChMIh-bCr_KmkwMVr2ZBAh3bBxdkEAAYASAAEgLsn_D_BwE&gbraid=0AAAAADQZUNR2eJLQTMRZPlD9xsDAbCiQB
IFCD110 Servicios en la nube (cloud) con AWS
40 horas online


Ficheros importantes de PHPMailer: Exception, smtp, php

https://ilias.avisando.com/practicas/reclamacionespro/


159, 160:
Nano banana de Gemini
Animar con Imagine de Grok
Unir animaciones con capcut



162: XAMPP, servidor web para probar la forma de enviar correo
163: FPDF, para generar PDF's
Larabee, un framework de PHP


Subir en GitHub nuestros propios proyectos/códigos


*** Día 18/03/2026


***

Del último manual J:

https://www.tramitapp.com/blog/ley-control-horario-espana/


tipo de horas
que estén las pausas
que se registren las modificaciones


Su tabla:
id cambio
n fact
hash
estado tv factura
respuesta
fecha


Mi programa llevaría
tabla de trabajadores: irrelevante, sólo datos personales y el idtrab interno de la empresa
horarios, tipo horarios o tarifas del patrón, la visible y enlazada con la anterior por el idtrab interno ese
la interna de horarios, que "codifica" la visible y enlaza los hash



El borrado en cascada (ON DELETE CASCADE) en phpMyAdmin elimina automáticamente los registros hijos en tablas relacionadas cuando se borra un registro padre, manteniendo la integridad referencial. Se configura definiendo una clave foránea (Foreign Key) en la tabla secundaria, seleccionando la opción CASCADE en la regla ON DELETE. 
SQL Shack
SQL Shack
 +3

Pasos para configurar Borrado en Cascada en phpMyAdmin
1. Requisitos: Ambas tablas deben usar el motor InnoDB.
2. Tabla Hija: Asegúrate de que la tabla dependiente tenga una columna que referencia a la tabla principal.
3. Vista de Relaciones: Ve a la tabla hija, haz clic en la pestaña Estructura y luego en Vista de relaciones.
4. Configurar Clave Foránea:
Crea una restricción (Constraint).
Selecciona la columna foránea (hija).
Selecciona la base de datos y la tabla principal (padre).
Selecciona la columna primaria (padre).
En ON DELETE, selecciona CASCADE.
5. Guardar: Clic en Guardar. 
SQL Shack
SQL Shack
 +4
Ejemplo de código SQL
sql

ALTER TABLE tabla_hija
ADD CONSTRAINT fk_nombre
FOREIGN KEY (columna_id) 
REFERENCES tabla_padre(id) 
ON DELETE CASCADE;


En mi caso en la tabla de fichajes tengo esto:
ALTER TABLE `conthorario_fichajes`
ADD FOREIGN KEY (`idtrabajador`)
REFERENCES `conthorario_trabajadores`(`id`)
ON DELETE CASCADE ON UPDATE RESTRICT;

Es decir, al montarlo desde conthorario_fichajes la tabla padre es trabajadores y la hija fichajes
Es decir, "elimina automáticamente los registros hijos en tablas relacionadas cuando se borra un registro padre" es:
elimina automáticamente los registros de fichajes en tablas relacionadas cuando se borra un registro de trabajador... Es decir, lo que buscaba.

Y es decir, la relación creada desde la tabla de trabajadores sería al revés, eliminar trabajadores al eliminar fichajes... No tiene sentido, elimino la relación de la tabla de trabajadores, ya he visto antes que es independiente de la de fichajes.
... o no, la dejo pero la paso a restrict, por ejemplo

***


¿Puedes generarme en PHP-JavaScript una aplicación de control horario que cumpla los requisitos de la web https://www.tramitapp.com/blog/ley-control-horario-espana/?


¡Claro! Aquí tienes una **aplicación completa de control horario** en **PHP + JavaScript** que cumple **todos los requisitos legales** de la Ley de Control Horario en España (Real Decreto-ley 8/2019, artículo 34.9 del Estatuto de los Trabajadores, y las actualizaciones digitales obligatorias de 2025-2026):

### ✅ Requisitos legales que CUMPLE esta app
- Registro **diario obligatorio** de hora exacta de **inicio y fin de jornada** (timestamp del servidor).
- Registro de **pausas** (opcional pero recomendado).
- **Trazabilidad total**: cada fichaje guarda IP, navegador, usuario y motivo de corrección (no se puede borrar ni modificar sin dejar rastro).
- Sistema **100 % digital** y fiable (nada de papel ni Excel).
- Acceso inmediato para trabajador, representantes y Inspección de Trabajo.
- Conservación de datos (mínimo 4 años – en esta app se guardan indefinidamente; en producción ajusta la política de borrado).
- Compatible con teletrabajo y movilidad.
- Cumple RGPD básico (datos por usuario, login seguro).

### Cómo instalarla (5 minutos)
1. Crea una carpeta `control-horario` en tu servidor (XAMPP, WAMP, hosting PHP 8+).
2. Crea una base de datos MySQL llamada `control_horario`.
3. Copia los archivos que te doy abajo.
4. Importa el archivo `database.sql`.
5. Abre `http://localhost/control-horario` → usuario admin: `admin` / contraseña: `admin123`.

### Archivos de la aplicación

#### 1. `database.sql` (ejecútalo una vez)
```sql
CREATE DATABASE IF NOT EXISTS control_horario;
USE control_horario;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(255),
    role ENUM('employee','admin') DEFAULT 'employee',
    full_name VARCHAR(100)
);

CREATE TABLE fichajes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    tipo ENUM('entrada','salida','pausa_inicio','pausa_fin','correccion'),
    fecha_hora DATETIME DEFAULT CURRENT_TIMESTAMP,
    ip VARCHAR(45),
    user_agent VARCHAR(255),
    motivo TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Usuarios de prueba
INSERT INTO users (username, password, role, full_name) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 'Administrador'), -- contraseña: admin123
('juan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Juan Pérez'),
('ana', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Ana García');
```

#### 2. `config.php`
```php
<?php
session_start();
$host = 'localhost';
$db   = 'control_horario';
$user = 'root';
$pass = ''; // cambia si tienes contraseña

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    die("Error conexión: " . $e->getMessage());
}
?>
```

#### 3. `api.php` (el cerebro de la app)
```php
<?php
require 'config.php';
header('Content-Type: application/json');

$action = $_GET['action'] ?? '';

if ($action === 'login') {
    $username = $_POST['username'];
    $pass = $_POST['password'];
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    
    if ($user && password_verify($pass, $user['password'])) {
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['role'] = $user['role'];
        $_SESSION['full_name'] = $user['full_name'];
        echo json_encode(['success' => true, 'role' => $user['role']]);
    } else {
        echo json_encode(['success' => false]);
    }
    exit;
}

if (!isset($_SESSION['user_id'])) {
    echo json_encode(['error' => 'No autorizado']);
    exit;
}

$user_id = $_SESSION['user_id'];

// Fichar
if ($action === 'fichar') {
    $tipo = $_POST['tipo']; // entrada, salida, pausa_inicio, pausa_fin
    $motivo = $_POST['motivo'] ?? '';
    
    $stmt = $pdo->prepare("INSERT INTO fichajes (user_id, tipo, ip, user_agent, motivo) 
                           VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([
        $user_id,
        $tipo,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $motivo
    ]);
    
    echo json_encode(['success' => true, 'mensaje' => 'Fichaje registrado correctamente']);
}

// Ver mis fichajes
if ($action === 'mis_fichajes') {
    $stmt = $pdo->prepare("SELECT * FROM fichajes WHERE user_id = ? ORDER BY fecha_hora DESC");
    $stmt->execute([$user_id]);
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Reporte admin (todos)
if ($action === 'reporte' && $_SESSION['role'] === 'admin') {
    $stmt = $pdo->prepare("SELECT f.*, u.full_name 
                           FROM fichajes f 
                           JOIN users u ON f.user_id = u.id 
                           ORDER BY f.fecha_hora DESC");
    $stmt->execute();
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Exportar CSV (admin)
if ($action === 'exportar' && $_SESSION['role'] === 'admin') {
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment; filename="control_horario_'.date('Y-m-d').'.csv"');
    
    $output = fopen('php://output', 'w');
    fputcsv($output, ['Nombre', 'Tipo', 'Fecha y hora', 'IP', 'Motivo']);
    
    $stmt = $pdo->query("SELECT u.full_name, f.tipo, f.fecha_hora, f.ip, f.motivo 
                         FROM fichajes f JOIN users u ON f.user_id = u.id");
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        fputcsv($output, $row);
    }
    exit;
}
?>
```

#### 4. `index.php` (la página principal – todo el frontend en JS)
```php
<?php require 'config.php'; ?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Control Horario España - Cumple Ley 2019/2026</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-light">
<div class="container py-5">
    <h1 class="text-center mb-4">⏰ Control Horario Legal España</h1>

    <?php if (!isset($_SESSION['user_id'])): ?>
    <!-- LOGIN -->
    <div class="card mx-auto" style="max-width: 400px;">
        <div class="card-body">
            <h5 class="card-title text-center">Iniciar sesión</h5>
            <input id="username" class="form-control my-2" placeholder="Usuario" value="admin">
            <input id="password" type="password" class="form-control my-2" placeholder="Contraseña" value="admin123">
            <button onclick="login()" class="btn btn-success w-100">Entrar</button>
        </div>
    </div>
    <?php else: ?>
    <div class="text-end">
        Bienvenido, <strong><?= htmlspecialchars($_SESSION['full_name']) ?></strong> 
        (<?= $_SESSION['role'] === 'admin' ? 'Administrador' : 'Empleado' ?>)
        <button onclick="logout()" class="btn btn-sm btn-outline-danger">Salir</button>
    </div>

    <div class="row mt-4">
        <!-- BOTONES DE FICHAJE -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-body text-center">
                    <h5>Fichar ahora</h5>
                    <button onclick="fichar('entrada')" class="btn btn-primary btn-lg m-2">🚪 ENTRADA</button>
                    <button onclick="fichar('salida')" class="btn btn-danger btn-lg m-2">🚪 SALIDA</button>
                    <button onclick="fichar('pausa_inicio')" class="btn btn-warning btn-lg m-2">☕ Pausa inicio</button>
                    <button onclick="fichar('pausa_fin')" class="btn btn-info btn-lg m-2">▶️ Pausa fin</button>
                </div>
            </div>
        </div>

        <!-- MIS REGISTROS -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">Mis fichajes recientes</div>
                <div class="card-body" id="misFichajes"></div>
            </div>
        </div>
    </div>

    <?php if ($_SESSION['role'] === 'admin'): ?>
    <div class="mt-5">
        <button onclick="cargarReporte()" class="btn btn-secondary">Ver reporte completo</button>
        <button onclick="exportarCSV()" class="btn btn-success">Exportar CSV (para Inspección)</button>
        <div id="reporte" class="mt-3"></div>
    </div>
    <?php endif; ?>
    <?php endif; ?>
</div>

<script>
// LOGIN
function login() {
    const form = new FormData();
    form.append('username', document.getElementById('username').value);
    form.append('password', document.getElementById('password').value);
    
    fetch('api.php?action=login', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => data.success ? location.reload() : alert('Credenciales incorrectas'));
}

// FICHAR
function fichar(tipo) {
    const motivo = tipo === 'correccion' ? prompt('Motivo de corrección:') : '';
    const form = new FormData();
    form.append('tipo', tipo);
    form.append('motivo', motivo);
    
    fetch('api.php?action=fichar', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => {
            alert(data.mensaje);
            cargarMisFichajes();
        });
}

// CARGAR MIS FICHAJES
function cargarMisFichajes() {
    fetch('api.php?action=mis_fichajes')
        .then(r => r.json())
        .then(data => {
            let html = '<ul class="list-group">';
            data.forEach(f => {
                html += `<li class="list-group-item">
                    <strong>${f.tipo.toUpperCase()}</strong> - ${f.fecha_hora}<br>
                    <small class="text-muted">IP: ${f.ip} | ${f.motivo ? 'Motivo: '+f.motivo : ''}</small>
                </li>`;
            });
            html += '</ul>';
            document.getElementById('misFichajes').innerHTML = html;
        });
}

// REPORTE ADMIN
function cargarReporte() {
    fetch('api.php?action=reporte')
        .then(r => r.json())
        .then(data => {
            let html = '<table class="table table-striped"><thead><tr><th>Nombre</th><th>Tipo</th><th>Fecha</th><th>IP</th><th>Motivo</th></tr></thead><tbody>';
            data.forEach(f => {
                html += `<tr><td>${f.full_name}</td><td>${f.tipo}</td><td>${f.fecha_hora}</td><td>${f.ip}</td><td>${f.motivo||''}</td></tr>`;
            });
            html += '</tbody></table>';
            document.getElementById('reporte').innerHTML = html;
        });
}

function exportarCSV() {
    window.location = 'api.php?action=exportar';
}

function logout() {
    sessionStorage.clear();
    location.reload();
}

// Cargar al iniciar
<?php if (isset($_SESSION['user_id'])): ?>
document.addEventListener('DOMContentLoaded', cargarMisFichajes);
<?php endif; ?>
</script>
</body>
</html>
```

### Listo para usar
- **Empleado**: solo ve sus fichajes y puede fichar.
- **Admin**: ve todo y exporta CSV (perfecto para Inspección de Trabajo).
- Todos los registros son **inalterables sin motivo** y guardan IP + navegador → 100 % trazable.

Si quieres añadir:
- Cálculo automático de horas extras
- Geolocalización (para teletrabajo)
- Notificaciones WhatsApp
- Multi-empresa

¡Dímelo y te lo añado en 2 minutos!

¿Quieres que te genere también la versión con SQLite (sin instalar MySQL) o con Docker? 😊


https://grok.com/c/d22f342b-4874-4533-89c6-0f83bb9e1d2e?rid=c1dba7eb-1c83-40f8-99cb-6bd5dcaa00b6


Tengo tablas users y fichajes, que renombraré como chorario_users y chorario_fichajes

34, 35: también en config.php tengo que poner los datos de mi bdd del curso (tras importar lo que exporté ayer. No sé si habría problemas en hacerlo al revés, pero por si acaso (duda))





A principios de 2026, Python y JavaScript compiten por el primer puesto como los lenguajes de programación más utilizados, dependiendo de si se mide por popularidad general o uso activo en desarrollo web. Python lidera en inteligencia artificial y análisis de datos, mientras que JavaScript domina el desarrollo web integral. 
KeepCoding
KeepCoding
 +2
Principales Lenguajes en 2026:
Python: El lenguaje más popular debido a su versatilidad, facilidad de aprendizaje y su papel fundamental en inteligencia artificial, Data Science y automatización KeepCoding, Nuclio Digital School.
JavaScript: El lenguaje fundamental para el desarrollo web (interactividad del lado del cliente y servidor) y uno de los más demandados en la actualidad Universidad InterNaciones, ISIL.
TypeScript: Una extensión de JavaScript muy popular que añade tipado estático, permitiendo detectar errores antes de la ejecución.
Java: Sigue siendo clave en aplicaciones empresariales de gran escala, sistemas Android y backend KeepCoding, Universidad Europea.
SQL: El estándar indispensable para la gestión y consulta de bases de datos relacionales YouTube, DataCamp. 
KeepCoding
KeepCoding
 +7
Otros lenguajes destacados:
C# / .NET: Muy fuerte en desarrollo Windows y videojuegos (Unity) KeepCoding, Universidad Europea.
C++: Utilizado en aplicaciones de alto rendimiento y motores gráficos.
Go (Golang): Destacado por su velocidad y manejo de concurrencia en backend KeepCoding. 
KeepCoding
KeepCoding
 +4
La elección depende de tu objetivo: Python para IA/datos, JavaScript para web, o Java/C# para aplicaciones corporativas. 
Pontia Tech
Pontia Tech


53: https://keepcoding.io/blog/lenguajes-de-programacion-mas-usados/
57: https://universidadeuropea.com/blog/lenguajes-programacion/#:~:text=Table_title:%20Comparativa%20entre%20lenguajes%20de%20programaci%C3%B3n%20Table_content:,%E2%80%8B%2D%20Aplicaciones%20cient%C3%ADficas%20y%20sistemas%20embebidos%20%7C
58: con Node.js... como decía Alfonso
59: parece ser Laravel, no Larabee como puse ayer

68: eso me lo pone por defecto al cambiar de pestaña

5,5

91, 92: ah, pues sí que pone que es "legal", oye
94: a investigar por qué me pone una clave

Ok:
            <input id="username" class="form-control my-2" placeholder="Usuario" value="admin">
            <input id="password" type="password" class="form-control my-2" placeholder="Contraseña" value="admin123">
En la casilla superior pone admin, y en la inferior los caracteres se corresponden con admin123... entiendo que el value se carga el placeholder

95: efectivamente, le quito el value="admin" y ya me saca el placeholder. Saca el placeholder algo más atenuado que el value
96: efectivamente, veo ya el value admin123 al quitar el type="password"







Ruth impresora
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi





    form.append('username', document.getElementById('username').value);


La línea de código form.append('username', document.getElementById('username').value); en JavaScript se utiliza para añadir un par clave-valor a un objeto FormData. 
MDN Web Docs
MDN Web Docs
Aquí tienes el desglose de qué hace exactamente:

document.getElementById('username').value:
Busca en el documento HTML un elemento (generalmente un input) que tenga el atributo id con el valor "username".
Obtiene el valor actual (.value) tecleado o seleccionado por el usuario en ese elemento.

form.append('username', ...):
Añade este par clave-valor al objeto FormData llamado form.
El nombre del campo (clave) en el formulario será 'username' y su valor será lo que se obtuvo en el paso anterior. 
MDN Web Docs
MDN Web Docs
 +3
¿Para qué sirve?
Generalmente, esto se usa antes de enviar datos al servidor mediante AJAX (fetch o XMLHttpRequest), sin necesidad de recargar la página.
Ejemplo completo:

// 1. Crear el objeto FormData
var form = new FormData();
// 2. Añaidr el valor del input con id 'username'
form.append('username', document.getElementById('username').value);
// 3. Enviar el objeto 'form' mediante fetch


Puntos clave:
Se usa principalmente para preparar datos para formularios (a menudo con archivos) que se envían por POST.
Si el input está vacío, append añadirá una cadena vacía ("") como valor.
Si el id no existe, el código dará un error (Cannot read properties of null) al intentar leer .value. 
MDN Web Docs
MDN Web Docs


fetch('api.php?action=login', {method: 'POST', body: form})

Esta línea de código JavaScript utiliza la Fetch API para enviar una petición HTTP de tipo POST de forma asíncrona a un servidor, sin necesidad de recargar la página web. 
YouTube
YouTube
 +1
Aquí está el desglose de qué hace exactamente:

fetch('api.php?action=login', ...): Llama al archivo api.php en el servidor, pasándole un parámetro GET ?action=login para indicar la acción a realizar.

method: 'POST': Define que los datos se enviarán ocultos en el cuerpo de la petición (body), lo cual es ideal para credenciales o datos sensibles.

body: form: Envía los datos contenidos en la variable form (que comúnmente es un objeto FormData que recoge los campos de un formulario HTML). 
YouTube
YouTube
 +3
En resumen:
Realiza una petición de inicio de sesión (login) enviando los datos de un formulario al backend (api.php) sin interrumpir la experiencia del usuario (sin recargar). 
Uso típico en JavaScript:

// 1. Obtener los datos del formulario HTML
const formElement = document.getElementById('loginForm');
const form = new FormData(formElement);

// 2. Enviar los datos
fetch('api.php?action=login', {
    method: 'POST',
    body: form
})
.then(response => response.json()) // 3. Procesar la respuesta del servidor
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

Generalmente, el servidor recibe estos datos con $_POST (si es PHP) y responde con un JSON indicando si el inicio de sesión fue exitoso o no. 


*** Día 18/03/2026 desde 17:30, torre 2

DATABASE.SQL
CREATE DATABASE IF NOT EXISTS control_horario;
USE control_horario;

CREATE TABLE chorario_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(255),
    role ENUM('employee','admin') DEFAULT 'employee',
    full_name VARCHAR(100)
);

CREATE TABLE chorario_fichajes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    tipo ENUM('entrada','salida','pausa_inicio','pausa_fin','correccion'),
    fecha_hora DATETIME DEFAULT CURRENT_TIMESTAMP,
    ip VARCHAR(45),
    user_agent VARCHAR(255),
    motivo TEXT,
    FOREIGN KEY (user_id) REFERENCES chorario_users(id)
);

-- Usuarios de prueba
INSERT INTO chorario_users (username, password, role, full_name) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 'Administrador'), -- contraseña: admin123
('juan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Juan Pérez'),
('ana', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Ana García');




CONFIG.PHP
<?php
session_start();
$host = 'localhost';
$db   = 'arturo';
$user = 'arturo';
$pass = 'arturo@123456'; // cambia si tienes otra contraseña

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    die("Error conexión: " . $e->getMessage());
}
?>


INDEX.PHP
<?php require 'config.php'; ?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Control Horario España - Cumple Ley 2019/2026</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-light">
<div class="container py-5">
    <h1 class="text-center mb-4">⏰ Aplicación Control Horario</h1>

    <?php if (!isset($_SESSION['user_id'])): ?>
    <!-- LOGIN -->
    <div class="card mx-auto" style="max-width: 400px;">
        <div class="card-body">
            <h5 class="card-title text-center">Iniciar sesión</h5>
            <input id="username" class="form-control my-2" placeholder="Usuario" value="admin">
            <input id="password" type="password" class="form-control my-2" placeholder="Contraseña" value="admin123">
            <button onclick="login()" class="btn btn-success w-100">Entrar</button>
        </div>
    </div>
    <?php else: ?>
    <div class="text-end">
        Bienvenido, <strong><?= htmlspecialchars($_SESSION['full_name']) ?></strong> 
        (<?= $_SESSION['role'] === 'admin' ? 'Administrador' : 'Empleado' ?>)
        <button onclick="logout()" class="btn btn-sm btn-outline-danger">Salir</button>
    </div>

    <div class="row mt-4">
        <!-- BOTONES DE FICHAJE -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-body text-center">
                    <h5>Fichar ahora</h5>
                    <button onclick="fichar('entrada')" class="btn btn-primary btn-lg m-2">🚪 ENTRADA</button>
                    <button onclick="fichar('salida')" class="btn btn-danger btn-lg m-2">🚪 SALIDA</button>
                    <button onclick="fichar('pausa_inicio')" class="btn btn-warning btn-lg m-2">☕ Pausa inicio</button>
                    <button onclick="fichar('pausa_fin')" class="btn btn-info btn-lg m-2">▶️ Pausa fin</button>
                </div>
            </div>
        </div>

        <!-- MIS REGISTROS -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">Mis fichajes recientes</div>
                <div class="card-body" id="misFichajes"></div>
            </div>
        </div>
    </div>

    <?php if ($_SESSION['role'] === 'admin'): ?>
    <div class="mt-5">
        <button onclick="cargarReporte()" class="btn btn-secondary">Ver reporte completo</button>
        <button onclick="exportarCSV()" class="btn btn-success">Exportar CSV (para Inspección)</button>
        <div id="reporte" class="mt-3"></div>
    </div>
    <?php endif; ?>
    <?php endif; ?>
</div>

<script>
// LOGIN
function login() {
    const form = new FormData();
    form.append('username', document.getElementById('username').value);
    form.append('password', document.getElementById('password').value);
    
    fetch('api.php?action=login', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => data.success ? location.reload() : alert('Credenciales incorrectas'));
}

// FICHAR
function fichar(tipo) {
    const motivo = tipo === 'correccion' ? prompt('Motivo de corrección:') : '';
    const form = new FormData();
    form.append('tipo', tipo);
    form.append('motivo', motivo);
    
    fetch('api.php?action=fichar', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => {
            alert(data.mensaje);
            cargarMisFichajes();
        });
}

// CARGAR MIS FICHAJES
function cargarMisFichajes() {
    fetch('api.php?action=mis_fichajes')
        .then(r => r.json())
        .then(data => {
            let html = '<ul class="list-group">';
            data.forEach(f => {
                html += `<li class="list-group-item">
                    <strong>${f.tipo.toUpperCase()}</strong> - ${f.fecha_hora}<br>
                    <small class="text-muted">IP: ${f.ip} | ${f.motivo ? 'Motivo: '+f.motivo : ''}</small>
                </li>`;
            });
            html += '</ul>';
            document.getElementById('misFichajes').innerHTML = html;
        });
}

// REPORTE ADMIN
function cargarReporte() {
    fetch('api.php?action=reporte')
        .then(r => r.json())
        .then(data => {
            let html = '<table class="table table-striped"><thead><tr><th>Nombre</th><th>Tipo</th><th>Fecha</th><th>IP</th><th>Motivo</th></tr></thead><tbody>';
            data.forEach(f => {
                html += `<tr><td>${f.full_name}</td><td>${f.tipo}</td><td>${f.fecha_hora}</td><td>${f.ip}</td><td>${f.motivo||''}</td></tr>`;
            });
            html += '</tbody></table>';
            document.getElementById('reporte').innerHTML = html;
        });
}

function exportarCSV() {
    window.location = 'api.php?action=exportar';
}

function logout() {
    sessionStorage.clear();
    location.reload();
}

// Cargar al iniciar
<?php if (isset($_SESSION['user_id'])): ?>
document.addEventListener('DOMContentLoaded', cargarMisFichajes);
<?php endif; ?>
</script>
</body>
</html>



API.PHP
<?php
require 'config.php';
header('Content-Type: application/json');

$action = $_GET['action'] ?? '';

if ($action === 'login') {
    $username = $_POST['username'];
    $pass = $_POST['password'];
    $stmt = $pdo->prepare("SELECT * FROM chorario_users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    
    if ($user && password_verify($pass, $user['password'])) {
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['role'] = $user['role'];
        $_SESSION['full_name'] = $user['full_name'];
        echo json_encode(['success' => true, 'role' => $user['role']]);
    } else {
        echo json_encode(['success' => false]);
    }
    exit;
}

if (!isset($_SESSION['user_id'])) {
    echo json_encode(['error' => 'No autorizado']);
    exit;
}

$user_id = $_SESSION['user_id'];

// Fichar
if ($action === 'fichar') {
    $tipo = $_POST['tipo']; // entrada, salida, pausa_inicio, pausa_fin
    $motivo = $_POST['motivo'] ?? '';
    
    $stmt = $pdo->prepare("INSERT INTO chorario_fichajes (user_id, tipo, ip, user_agent, motivo) 
                           VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([
        $user_id,
        $tipo,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $motivo
    ]);
    
    echo json_encode(['success' => true, 'mensaje' => 'Fichaje registrado correctamente']);
}

// Ver mis fichajes
if ($action === 'mis_fichajes') {
    $stmt = $pdo->prepare("SELECT * FROM chorario_fichajes WHERE user_id = ? ORDER BY fecha_hora DESC");
    $stmt->execute([$user_id]);
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Reporte admin (todos)
if ($action === 'reporte' && $_SESSION['role'] === 'admin') {
    $stmt = $pdo->prepare("SELECT f.*, u.full_name 
                           FROM chorario_fichajes f 
                           JOIN chorario_users u ON f.user_id = u.id 
                           ORDER BY f.fecha_hora DESC");
    $stmt->execute();
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Exportar CSV (admin)
if ($action === 'exportar' && $_SESSION['role'] === 'admin') {
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment; filename="control_horario_'.date('Y-m-d').'.csv"');
    
    $output = fopen('php://output', 'w');
    fputcsv($output, ['Nombre', 'Tipo', 'Fecha y hora', 'IP', 'Motivo']);
    
    $stmt = $pdo->query("SELECT u.full_name, f.tipo, f.fecha_hora, f.ip, f.motivo 
                         FROM chorario_fichajes f JOIN chorario_users u ON f.user_id = u.id");
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        fputcsv($output, $row);
    }
    exit;
}
?>


*** Día 19/03/2026


https://uni.edu.gt/noticias/lenguaje-de-programacion/
https://universidadeuropea.com/blog/lenguajes-programacion/#:~:text=Table_title:%20Comparativa%20entre%20lenguajes%20de%20programaci%C3%B3n%20Table_content:,%E2%80%8B%2D%20Aplicaciones%20cient%C3%ADficas%20y%20sistemas%20embebidos%20%7C
https://keepcoding.io/blog/lenguajes-de-programacion-mas-usados/
https://fpnebrija.com/blog/2025/03/10/los-lenguajes-de-programacion-mas-usados-en-espana/

Los 2 centrales eran de ayer

De lo que ha desaparecido ayer recuerdo:
Que si tienes placeholder y value el value prima sobre el placeholder, y se muestra nítido mientras que el placeholder está atenuado
Que el type="password" es lo que hace que no se muestre el contenido de la casilla... ¿Y cómo es para cambiar cuando lo haces visible?... Tal vez que el type sea variable, se me ocurre

También me falta el PDF del programa que me dio grok


Y tuve que repetir el sftp.json del sftp:config

{
    "name": "arturo.avisando.com",
    "host": "arturo.avisando.com",
    "protocol": "sftp",
    "port": 22,
    "username": "arturo",
    "remotePath": "/home/arturo/public_html/",
    "uploadOnSave": true,
    "useTempFile": false,
    "openSsh": false
}



37: mira, esto me gusta, que me dé error en el intento de acceso directo
38: y por aquí me andaba ayer cuando se fundió el otro equipo, la otra torre

Google me dice que use esta expresión:
echo "<script>console.log(" . json_encode($variable) . ");</script>";

Así que mi experimento de ayer debe cambiar:
echo "console.log ($username)";
echo "<script>console.log(" . json_encode($username) . ");</script>";
echo "<script>console.log(" . json_encode($pass) . ");</script>";

39: pues ya ves, sigue con lo mismo
40: bueno, al menos pinchando en 39 en lo de VM52:1 me trae aquí y veo lo que busco: que al menos aquí no me codifica el password. Pero sigo sin saber cómo mostrar información en la consola...

42: pues nada, que no codifica el password, parece que sigue igual... pero no me funcionaba el acceso
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi
<script>console.log("$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC\/.og\/at2.uheWG\/igi");</script>{"success":false}

A ver si va a ser por las barras que ha metido la consulta preparada... Voy a probar cambiando en la bdd esa clave por una más "ortodoxa", sin barras

UPDATE `chorario_users` SET `password` = '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC.ogat2.uheWGigi' WHERE `chorario_users`.`id` = 1;
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC.ogat2.uheWGigi

45 a 47: pues que no, que otra vez mal esto, parece:
<script>console.log("$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC.ogat2.uheWGigi");</script>{"success":false}

49, 50: otra vez credenciales incorrectas, y he cambiado la clave a marte... el problema estará en el código, no tiene sentido

51, 52: ok, no me funciona la consola con echo "<script>console.log(" . json_encode($user['password']) . ");</script>"; pero veo que $user['password'] me recoge bien lo de la bdd y es igual que $pass, así que entiendo que el problema está en eso de password_verify($pass, $user['password'])

55: aparentemente lo estoy usando bien, según https://es.stackoverflow.com/questions/460888/c%C3%B3mo-utilizar-password-verify... pero me ha sorprendido lo de 53, así que... 56 y siguientes

61: https://imgs.xkcd.com/comics/exploits_of_a_mom.png

64: pues mira, es un varchar(255)… voy a probar cargándome el verify este, por ejemplo, y volviendo al password original: $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi

67: EFECTIVAMENTE, el problema era el maldito verify, porque con esto ya funciona (barras incluidas):
    //if ($user && password_verify($pass, $user['password'])) {
    if ($user && ($pass == $user['password'])) {

Es decir, que después de todo esto... ya he conseguido que ENTRE en el programa

https://www.php.net/manual/es/function.password-verify.php

69, 70: ok, que parece que la IA me ha metido el verify sin usar hash, por eso fallaba. Ya me sonaban estas claves a encriptadas...

https://www.php.net/manual/es/function.password-hash.php

... y 72, que no hay forma de revertir el proceso y ver cuáles son las supuestas verdaderas claves que la IA había pensado para los usuarios. Y encima el log out no me funciona

74: borrar fichero de cookies chrome

Para borrar las cookies en Chrome, abre el navegador y presiona Ctrl + Shift + Supr (Windows) o Cmd + Shift + Borrar (Mac), o ve al menú de tres puntos > "Más herramientas" > "Borrar datos de navegación". Selecciona el rango de tiempo (ej. "Todos"), marca "Cookies y otros datos de sitios" y haz clic en "Borrar datos". 
Google Help
Google Help
 +4
Este video te mostrará cómo borrar las cookies en Google Chrome:

Pasos detallados para borrar cookies en Chrome:
Método rápido (Escritorio): Presiona Ctrl + Shift + Supr (o Cmd + Shift + Delete en Mac) para abrir directamente la ventana de borrado.

A través del menú:
Abre Chrome y haz clic en los tres puntos (esquina superior derecha).
Selecciona Más herramientas y luego Borrar datos de navegación.
En la ventana emergente, elige el Intervalo de tiempo (última hora, últimos 7 días, o Todo el período).
Marca la casilla Cookies y otros datos de sitios.
Haz clic en Borrar datos.

Borrar cookies de un sitio específico:
Ve a chrome://settings/siteData en la barra de direcciones.
Busca el sitio web específico.
Haz clic en el icono de la papelera (Eliminar) junto al sitio.

En Android:
Abre Chrome y toca los tres puntos > Configuración.
Ve a Privacidad y seguridad > Borrar datos de navegación.
Selecciona el intervalo y marca Cookies y datos de sitios.
Toca Borrar datos. 
Google Help
Google Help
 +7
Nota: Al borrar las cookies, cerrarás la sesión en la mayoría de los sitios web. 
Google Help
Google Help


82: hasta que no borro esto no me devuelve a la página de login


UPDATE `chorario_users` SET `password` = 'admin1234' WHERE `chorario_users`.`id` = 1;
UPDATE `chorario_users` SET `password` = 'admin1234' WHERE `chorario_users`.`id` = 2;
UPDATE `chorario_users` SET `password` = 'admin1234' WHERE `chorario_users`.`id` = 3;


106:
Para ver cookies y ficheros de sesión en Chrome, haz clic derecho en cualquier parte de una web, selecciona "Inspeccionar", ve a la pestaña "Application" (Aplicación) y expande la sección "Cookies" en el menú izquierdo. Podrás visualizar nombre, valor, dominio y caducidad de cookies propias y de terceros. 
YouTube
YouTube
 +3
Este video muestra cómo acceder a la herramienta de inspección de Chrome:

Pasos detallados:
Abrir el Inspector: Presiona F12 o clic derecho > Inspeccionar.
Pestaña Aplicación: Selecciona "Application" (arriba). Si no la ves, haz clic en las flechas >> para ver más opciones.
Cookies: En el menú lateral izquierdo, busca la sección "Storage" (Almacenamiento) y despliega "Cookies".
Seleccionar Sitio: Haz clic en la URL del sitio bajo el menú "Cookies" para ver el desglose. 
YouTube
YouTube
 +2
Qué información puedes encontrar:
Nombre y Valor: El dato de la cookie.
Domain (Dominio): Qué servidor utiliza la cookie.
Expires / Max-Age: Cuándo se borra.
Session Storage/Local Storage: En el mismo panel "Application", puedes ver datos de sesión almacenados localmente.
Eliminar Cookies: Haz clic derecho sobre una cookie y selecciona "Delete" para borrarla. 
YouTube
YouTube
 +2
Alternativa rápida: Ve a chrome://settings/siteData para ver las cookies guardadas por todos los sitios. 
Google Help
Google Help
 +2


107: vale, si borro el fichero de sesión ya me deja salir, pero al recargar la ventana de login me crea otra... ¿es normal?
108: sí, parece que es normal
110: voy a probar a eliminar los paréntesis de la línea 28... pues no es eso... porque son simple texto, son paréntesis sin más

En un href con la barra al principio la rama siguiente no te lo une al directorio donde está el origen, sino al anterior, según veo en el equipo de Jose (rev, probar por mí mismo)

111 me lleva a 112

115 tampoco funciona, volveré al código anterior

118: y esto me da error, tampoco me sirve... habrá que ver programas anteriores como los del Patrón


https://lenguajejs.com/javascript/web-apis/localstorage/

Sospecho que va a ser algo de esto:
https://stackoverflow.com/questions/33049826/sessionstorage-clear-not-working
cuando dice que "Creo que se debe a que la redirección se realiza en el servidor, mientras que la sessionStorage.clear()operación se ejecuta en el cliente. Me parece que la redirección se produce antes de que la operación se complete"


La web propone esto, tal vez mañana lo intente con esto:

<?php
session_start();
session_destroy();
<script type="text/javascript">

function firstFunction(_callback){
    sessionStorage.clear();
    _callback();
}

function secondFunction(){
    firstFunction(function() {
        window.location = './';
    });    
}
secondFunction();
</script>


Este es el copiado desde la web, "con atribuciones":

// Source - https://stackoverflow.com/a/33055538
// Posted by Tom
// Retrieved 2026-03-19, License - CC BY-SA 3.0

<?php
session_start();
session_destroy();
<script type="text/javascript">

function firstFunction(_callback){
    sessionStorage.clear();
    _callback();
}

function secondFunction(){
    firstFunction(function() {
        window.location = './';
    });    
}
secondFunction();
</script>


*** Día 20/03/2026

Llevamos 32 horas cumplidas, quedan 48 horas más. Queda el 60%, traducido en 7 días 7, de la ganadería de PHP Javascript
Nos queda alguna mesa viva y el demonio aún no nos ha tirado el techo encima, eso que llevamos avanzado. Pero nos dicen que va a venir, que el demonio va a venir


8, 9: lo dejo así... pero no veo que me borre lo de la sesión, y tampoco me reacciona la web
function logout() {
    sessionStorage.clear();
    //location.reload();
}


Pues venga, a adaptar lo de ayer por la tarde, a ver si tal:
<?php
session_start();
session_destroy();
<script type="text/javascript">

function firstFunction(_callback){
    sessionStorage.clear();
    _callback();
}

function secondFunction(){
    firstFunction(function() {
        window.location = './';
    });    
}
secondFunction();
</script>


Entiendo que adaptado quedaría tal que así:

function logout() {

<script type="text/javascript">

function firstFunction(_callback){
    sessionStorage.clear();
    _callback();
}

function secondFunction(){
    firstFunction(function() {
        window.location = './';
    });    
}
secondFunction();
</script>

}

10: pues no, me da error hasta que lo dejo así:

function logout() {

function firstFunction(_callback){
    sessionStorage.clear();
    _callback();
}

function secondFunction(){
    firstFunction(function() {
        window.location = './';
    });    
}
secondFunction();

}

Y volvemos a lo mismo, que hasta que no borro manualmente el fichero de sesión nada de nada. Habrá que revisar los logouts de otros programas. Y luego reformar el programa en general, que esa es otra
Por de pronto voy a dejar esta function como estaba

18: se me ocurre un nuevo intento, anulando el window.location ese... y nada, que no me cierra el tema
Ahora incluso borro el fichero de sesión y le doy al log out y no me vuelve a la ventana de login hasta que no actualizo la página... Es decir, que no va


32: aparte de ir por cookie veo que el cierre de la web del patrón va en un fichero diferente... empezaré por eso, por separar el cierre, a ver qué tal. Entiendo que el movimiento entre archivos aumentará el tiempo que pueda necesitar la aplicación para cerrar sesión




<?php
setcookie('id', '', time() - 500);
header("Location: index.php");
exit();
?>


Uncaught chorario/:17 ReferenceError: logout is not defined at HTMLButtonElement.onclick (chorario/:17:74) onclick @ chorario/:17

Este error indica que el navegador está intentando ejecutar una función llamada logout() cuando haces clic en un botón, pero no logra encontrar su definición en el código JavaScript cargado. Es un fallo de referencia común que detiene la ejecución de esa acción específica. 
YouTube
YouTube
 +3
Causas comunes y cómo solucionarlas

Nombre mal escrito: JavaScript distingue entre mayúsculas y minúsculas. Revisa que el nombre en el HTML (onclick="logout()") coincida exactamente con el del script (ej. si escribiste LogOut o logOut en el archivo .js, fallará).

La función no es global: Si definiste logout() dentro de otra función o dentro de un bloque como $(document).ready(), el botón de HTML no podrá "verla". Debe estar declarada en el ámbito global (fuera de cualquier llave {}) para que el atributo onclick funcione.

Archivo JS no cargado: Es posible que el archivo que contiene la función no se esté cargando correctamente debido a una ruta incorrecta en la etiqueta <script src="..."> o un error de red.

Error previo en el script: Si hay un error de sintaxis antes de la definición de logout(), el navegador dejará de leer el archivo y nunca llegará a registrar la función.

Orden de carga: Asegúrate de que el script se cargue antes de que el usuario pueda interactuar con el botón, o utiliza addEventListener en lugar de onclick directo en el HTML para evitar problemas de ámbito. 
Stack Overflow
Stack Overflow
 +6

¿En qué archivo o parte de tu código tienes definida la función logout? Quizás podamos revisar si el ámbito de la variable es el correcto. 
Las respuestas de la IA pueden contener errores. Más información


42: anulando el header no da error
43: ah, oye, que reactivo la expresión para confirmar y al menos ya me lleva al logout.php, aunque me salta este error
50 a 52: ah, que resulta que se va moviendo de un lado a otro... hay que meter un filtro en 55, de alguna manera
58: lo de 56 me sirve para solucionar 57
<?php endif; ?>
Es decir, entiendo que estoy en JS y que en esta tierra necesito un endif, como en Clipper creo que era (ver 70, 71)

Lo de 59 debe venir de 55, este error:
Parse error: syntax error, unexpected end of file, expecting "elseif" or "else" or "endif" in /home/arturo/public_html/practicas/chorario/index.php on line 145

60: nada, volvemos al círculo sin fin... probaré quitándole el ! al IF de esta function
61: por ahora premio, vuelvo a ver la ventana de login. Ahora a ver si salgo bien una vez dentro

63: hala, otra vez el error de antes al intentar salir... lo de Uncaught chorario/:17 ReferenceError: logout is not defined at HTMLButtonElement.onclick (chorario/:17:74) onclick @ chorario/:17

65: consigo salir borrando la variable de sesión y actualizando la web, porque no salgo del error ese... y al intentar entrar veo también este error: Uncaught index.php:119:9 SyntaxError: Unexpected token '<' (at index.php:119:9)

67: <b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /home/arturo/public_html/practicas/chorario/index.php:63) in <b>/home/arturo/public_html/practicas/chorario/index.php</b> on line <b>135</b><br />

Línea 135 aquí no hay, y la de mi código es <?php header("Location: logout.php");  ?>
69: esta línea 63 tampoco me cuadra, y la de mi código es... </div>, la etiqueta de fin de carga de los botones del administrador

Entonces entiendo algo que de nuevo no me gusta: que le da por ejecutarme las funciones también al cargar la página, no sólo cuando la llamo... antes con el círculo vicioso y ahora cuando estoy logado...
Me temo que tengo que mandar TODO al otro fichero, que el onclick me mande directamente al logout.php. Y eso además sería más lógico

70, 71: pues pueden ser diferentes sintaxis en PHP
https://es.stackoverflow.com/questions/115748/diferencia-entre-ifcondicion-else-endif-y-ifcondicionelse-en-php
72, 73: El uso de endif es típico de otros lenguajes como PHP (en plantillas), Python o Bash, pero no en JavaScript.

Las dos funciones que quito:

//function logout() {
    //sessionStorage.clear();
    //location.reload();
//}


function logout() {
    <?php if (isset($_SESSION['user_id'])): ?>
    <?php header("Location: logout.php");  ?>
    <?php endif; ?>
}


83: no me deja rechazar todo, qué astutos

https://arturo.avisando.com/practicas/chorario/index.php

93, 94: con esto intentaba que fuera más lento y mira, al final es incluso más rápido... pues se me ocurre que el logout.php se quede ahí, hasta que alguien decida volver al login… o jamás.
Y ahora me pregunto ¿cómo lo hacen los bancos???? Porque ellos sí que vuelven al login tan normales
95: pillado al vuelo, porque incluso saca este par más durante unos instantes


99, 100: ok, así me va al logout, perfecto. Me queda ver si cierra sesión DE UNA VEZ, JOHNNY

101: incluso si abro otra ventana aparte me sigue manteniendo la maldita sesión, esto parece una maldición de Stephen King... Hay que DESTRUIR esa sesión
106: unset tampoco me sirve... probaré a cambiar el valor, pero es que tengo varios porque es un array...

117: https://eitca.org/web-development/eitc-wd-pmsf-php-and-mysql-fundamentals/expertise-in-php/sessions/examination-review-sessions/how-can-we-delete-a-session-variable-in-php/
desde https://eitca.org/web-development/eitc-wd-pmsf-php-and-mysql-fundamentals/expertise-in-php/sessions/examination-review-sessions/how-can-we-access-the-value-stored-in-a-session-variable/#:~:text='username'%5D;-,php%20session_start();%20echo%20$_SESSION%5B'username'%5D;,value%20on%20the%20web%20page.


session_unset();

… parece que nada funciona, no lo entiendo. Habrá que prescindir de esto de las sesiones y pasar a las cookies


database.sql

CREATE DATABASE IF NOT EXISTS control_horario;
USE control_horario;

CREATE TABLE chorario_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(255),
    role ENUM('employee','admin') DEFAULT 'employee',
    full_name VARCHAR(100)
);

CREATE TABLE chorario_fichajes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    tipo ENUM('entrada','salida','pausa_inicio','pausa_fin','correccion'),
    fecha_hora DATETIME DEFAULT CURRENT_TIMESTAMP,
    ip VARCHAR(45),
    user_agent VARCHAR(255),
    motivo TEXT,
    FOREIGN KEY (user_id) REFERENCES chorario_users(id)
);

-- Usuarios de prueba
INSERT INTO chorario_users (username, password, role, full_name) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 'Administrador'), -- contraseña: admin123
('juan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Juan Pérez'),
('ana', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'employee', 'Ana García');



config.php
<?php
session_start();
$host = 'localhost';
$db   = 'arturo';
$user = 'arturo';
$pass = 'arturo@123456'; // cambia si tienes otra contraseña

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    die("Error conexión: " . $e->getMessage());
}
?>




index.php
<?php require 'config.php'; ?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Control Horario España - Cumple Ley 2019/2026</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-light">
<div class="container py-5">
    <h1 class="text-center mb-4">⏰ Aplicación Control Horario</h1>

    <?php if (!isset($_SESSION['user_id'])): ?>
    <!-- LOGIN -->
    <div class="card mx-auto" style="max-width: 400px;">
        <div class="card-body">
            <h5 class="card-title text-center">Iniciar sesión</h5>
            <input id="username" class="form-control my-2" placeholder="Usuario" value="admin">
            <input id="password" type="password" class="form-control my-2" placeholder="Contraseña" value="admin123">
            <button onclick="login()" class="btn btn-success w-100">Entrar</button>
        </div>
    </div>
    <?php else: ?>
    <div class="text-end">
        Bienvenido, <strong><?= htmlspecialchars($_SESSION['full_name']) ?></strong> 
        (<?= $_SESSION['role'] === 'admin' ? 'Administrador' : 'Empleado' ?>)
        <button onclick="logout()" class="btn btn-sm btn-outline-danger">Salir</button>
    </div>

    <div class="row mt-4">
        <!-- BOTONES DE FICHAJE -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-body text-center">
                    <h5>Fichar ahora</h5>
                    <button onclick="fichar('entrada')" class="btn btn-primary btn-lg m-2">🚪 ENTRADA</button>
                    <button onclick="fichar('salida')" class="btn btn-danger btn-lg m-2">🚪 SALIDA</button>
                    <button onclick="fichar('pausa_inicio')" class="btn btn-warning btn-lg m-2">☕ Pausa inicio</button>
                    <button onclick="fichar('pausa_fin')" class="btn btn-info btn-lg m-2">▶️ Pausa fin</button>
                </div>
            </div>
        </div>

        <!-- MIS REGISTROS -->
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">Mis fichajes recientes</div>
                <div class="card-body" id="misFichajes"></div>
            </div>
        </div>
    </div>

    <?php if ($_SESSION['role'] === 'admin'): ?>
    <div class="mt-5">
        <button onclick="cargarReporte()" class="btn btn-secondary">Ver reporte completo</button>
        <button onclick="exportarCSV()" class="btn btn-success">Exportar CSV (para Inspección)</button>
        <div id="reporte" class="mt-3"></div>
    </div>
    <?php endif; ?>
    <?php endif; ?>
</div>

<script>
// LOGIN
function login() {
    const form = new FormData();
    form.append('username', document.getElementById('username').value);
    form.append('password', document.getElementById('password').value);
    
    fetch('api.php?action=login', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => data.success ? location.reload() : alert('Credenciales incorrectas'));
}

// FICHAR
function fichar(tipo) {
    const motivo = tipo === 'correccion' ? prompt('Motivo de corrección:') : '';
    const form = new FormData();
    form.append('tipo', tipo);
    form.append('motivo', motivo);
    
    fetch('api.php?action=fichar', {method: 'POST', body: form})
        .then(r => r.json())
        .then(data => {
            alert(data.mensaje);
            cargarMisFichajes();
        });
}

// CARGAR MIS FICHAJES
function cargarMisFichajes() {
    fetch('api.php?action=mis_fichajes')
        .then(r => r.json())
        .then(data => {
            let html = '<ul class="list-group">';
            data.forEach(f => {
                html += `<li class="list-group-item">
                    <strong>${f.tipo.toUpperCase()}</strong> - ${f.fecha_hora}<br>
                    <small class="text-muted">IP: ${f.ip} | ${f.motivo ? 'Motivo: '+f.motivo : ''}</small>
                </li>`;
            });
            html += '</ul>';
            document.getElementById('misFichajes').innerHTML = html;
        });
}

// REPORTE ADMIN
function cargarReporte() {
    fetch('api.php?action=reporte')
        .then(r => r.json())
        .then(data => {
            let html = '<table class="table table-striped"><thead><tr><th>Nombre</th><th>Tipo</th><th>Fecha</th><th>IP</th><th>Motivo</th></tr></thead><tbody>';
            data.forEach(f => {
                html += `<tr><td>${f.full_name}</td><td>${f.tipo}</td><td>${f.fecha_hora}</td><td>${f.ip}</td><td>${f.motivo||''}</td></tr>`;
            });
            html += '</tbody></table>';
            document.getElementById('reporte').innerHTML = html;
        });
}

function exportarCSV() {
    window.location = 'api.php?action=exportar';
}


function logout(){
    sessionStorage.clear();
    location.reload();
}


// Cargar al iniciar
<?php if (isset($_SESSION['user_id'])): ?>
document.addEventListener('DOMContentLoaded', cargarMisFichajes);
<?php endif; ?>
</script>
</body>
</html>




api.php
<?php
require 'config.php';
header('Content-Type: application/json');

$action = $_GET['action'] ?? '';

if ($action === 'login') {
    $username = $_POST['username'];
    $pass = $_POST['password'];
    $stmt = $pdo->prepare("SELECT * FROM chorario_users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    //echo "<script>console.log(" . json_encode($pass) . ");</script>";    
    
    //if ($user && password_verify($pass, $user['password'])) {
    if ($user && ($pass == $user['password'])) {
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['role'] = $user['role'];
        $_SESSION['full_name'] = $user['full_name'];
        echo json_encode(['success' => true, 'role' => $user['role']]);
    } else {
        //echo "<script>console.log(" . json_encode($user['password']) . ");</script>";
        echo json_encode(['success' => false]);
    }
    exit;
}

if (!isset($_SESSION['user_id'])) {
    echo json_encode(['error' => 'No autorizado']);
    exit;
}

$user_id = $_SESSION['user_id'];

// Fichar
if ($action === 'fichar') {
    $tipo = $_POST['tipo']; // entrada, salida, pausa_inicio, pausa_fin
    $motivo = $_POST['motivo'] ?? '';
    
    $stmt = $pdo->prepare("INSERT INTO chorario_fichajes (user_id, tipo, ip, user_agent, motivo) 
                           VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([
        $user_id,
        $tipo,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $motivo
    ]);
    
    echo json_encode(['success' => true, 'mensaje' => 'Fichaje registrado correctamente']);
}

// Ver mis fichajes
if ($action === 'mis_fichajes') {
    $stmt = $pdo->prepare("SELECT * FROM chorario_fichajes WHERE user_id = ? ORDER BY fecha_hora DESC");
    $stmt->execute([$user_id]);
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Reporte admin (todos)
if ($action === 'reporte' && $_SESSION['role'] === 'admin') {
    $stmt = $pdo->prepare("SELECT f.*, u.full_name 
                           FROM chorario_fichajes f 
                           JOIN chorario_users u ON f.user_id = u.id 
                           ORDER BY f.fecha_hora DESC");
    $stmt->execute();
    echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}

// Exportar CSV (admin)
if ($action === 'exportar' && $_SESSION['role'] === 'admin') {
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment; filename="control_horario_'.date('Y-m-d').'.csv"');
    
    $output = fopen('php://output', 'w');
    fputcsv($output, ['Nombre', 'Tipo', 'Fecha y hora', 'IP', 'Motivo']);
    
    $stmt = $pdo->query("SELECT u.full_name, f.tipo, f.fecha_hora, f.ip, f.motivo 
                         FROM chorario_fichajes f JOIN chorario_users u ON f.user_id = u.id");
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        fputcsv($output, $row);
    }
    exit;
}
?>


*** Día 23/03/2026

El viernes quedamos que nada de esto funciona:


<script>
    //sessionStorage.clear();
    session_start();
    $_SESSION['value'] = "876";
    print_r($_SESSION);
    var_dump($_SESSION);
    session_unset();
    session_destroy();
</script>

<?php

// header("Location: index.php");

?>


El cierre de la web del patrón era así, con cookies:

<?php
setcookie('id', '', time() - 500);
header("Location: index.php");
exit();
?>



https://patronfightclub.com/


https://www.deepseek.com/
https://www.deepseek.com/en/

No, la IA está aquí:
https://use.ai/es?model=deepseek&utm_match_type=e&utm_funnel=&partner=WM&id=Z29vZ2xlfGNwY3xFUy1FUy1EZXNrdG9wLVNlYXJjaC1Vc2VBSXwyMzEzODU3OTI3MXxkZWVwc2VlayBlbiBlc3Bhw7FvbHw3OTE4MTM4MzQ4Mjh8fDE5MTAwMzgwNjQ2OXxFUy1FUy1EZWVwc2Vlay1HZW5lcmljLUV4YWN0fDc5MTgxMzgzNDgyOHx8fHx8fEVBSWFJUW9iQ2hNSXYtUGU4dFcxa3dNVlI3S0RCeDFOQWpMUUVBQVlBU0FCRWdMaWdmRF9Cd0V8fHw&url=https%3A%2F%2Fuse.ai%2Fes%3Fmodel%3Ddeepseek&gad_source=1&gad_campaignid=23138579271&gbraid=0AAAAACQlLobUEuE4CKBwvJ6NqxO6NBdOq



Y para la IA deepseek le hago un reunido.txt con el código

Tengo un problema con la aplicación del txt que te adjunto: no me cierra sesión. ¿Puedes decirme dónde está el problema?


Sí: el problema está en cómo implementaste el logout (cierre de sesión) dentro del archivo index.php.  

🧩 Análisis del problema

En tu aplicación:
• La sesión PHP se inicia en config.php con sessionstart().
• El inicio de sesión (login) se maneja correctamente en api.php con $SESSION.
• Pero el botón "Salir" del frontend solo ejecuta código JavaScript, no destruye la sesión en el servidor.

El código actual de cierre es este:

``js
function logout(){
    sessionStorage.clear();
    location.reload();
}
`

Ese código solo borra el sessionStorage del navegador, pero no elimina la sesión PHP del lado del servidor, por lo que $SESSION['userid'] sigue activa y el usuario continúa logueado.  
Por eso, al “recargar” (location.reload()), la app vuelve a mostrar el panel de usuario.

✅ Solución: agregar un endpoint de logout en api.php

Añade este bloque al final de api.php (antes del ?>):

`php
if ($action === 'logout') {
    sessionunset();
    sessiondestroy();
    echo jsonencode(['success' => true]);
    exit;
}
`

🔁 Modificar la función logout() en index.php

Cambia tu función JS de esta forma:

`js
function logout() {
    fetch('api.php?action=logout')
        .then(r => r.json())
        .then(() => location.reload());
}
`

De esta manera:
• Se llama al backend.
• PHP destruye la sesión real.
• Luego, al recargar, $SESSION['user_id'] ya no existirá y se mostrará el formulario de login.

💡 (Opcional) Limpieza adicional
Si quieres asegurarte de que no quede nada en el navegador (aunque no lo uses realmente):

`js
function logout() {
    fetch('api.php?action=logout')
        .then(r => r.json())
        .then(() => {
            sessionStorage.clear();
            location.reload();
        });
}
`

🧾 En resumen
| Problema | Causa | Solución |
|-----------|--------|-----------|
| No se cierra sesión | Solo se limpiaba sessionStorage, no la sesión PHP | Crear acción logout en api.php y llamarla desde logout()` |

Esto bastará para que el cierre de sesión funcione correctamente.





Y además tengo que desvincular ese logout.php experimental mío:
       <button onclick="window.location.href='logout.php';" class="btn btn-sm btn-outline-danger">Salir</button>

Lo dejo de esta forma:
       <button onclick="logout()" class="btn btn-sm btn-outline-danger">Salir</button>


27, 28: ya funciona, aunque ese valor de PHPSESSID parece el mismo, no me lo esperaba...
Y eso, que no era tema de que el sessionStorage.clear(); tardara más en ejecutarse que el location.reload();


50 a 61:
lo bueno es que te deja pulsar y registra el nuevo fichaje... y lo siguiente es lo que falta
Un relojito del tiempo que lleva el trabajador en ese estado
Un campo de estado en el trabajador para que no se puedan hacer fichajes incompatibles
El desglose de pausas para poder elegir la correcta (combinado con lo anterior)
Lo anterior me llevará a meter más utf8's y a tener cuidado con el recuadro
La posibilidad de sacar un listado desde el trabajador
… y me temo que le dará al listado de "últimos fichajes" por alargarse, así que cuidado con eso


72, 73: debería dejar imprimir lo que llama reporte completo... pero filtrando por trabajador, que me imagino que aquí me llevará todo lo que encuentre


At work - Trabajando
Fin jornada

Desayuno
Formación
Reunión interna
Pausa sin determinar

Externo - Inspeccionando


79: https://www.utf8icons.com/#google_vignette


Quiero añadirle a function fichar(tipo) otro parámetro llamado pausa que añada un valor a un campo llamado pausa, de la tabla chorario_users


87:
Para copiar datos de un campo específico entre tablas existentes en SQL, utiliza UPDATE con un JOIN (o subconsulta) para mapear los registros basados en una clave común, asegurando que los datos se transfieran correctamente de la tabla origen a la destino. 
IONOS
IONOS
 +1
sql
UPDATE tabla_destino
SET tabla_destino.campo_destino = tabla_origen.campo_origen
FROM tabla_origen
WHERE tabla_destino.id = tabla_origen.id;
Pasos clave:
UPDATE: Define la tabla que recibirá los datos.
SET: Especifica el campo de destino y qué campo de origen lo llenará.
FROM: Identifica la tabla que tiene los datos originales.
WHERE: Es crucial para emparejar la fila correcta de la tabla origen con la de destino, evitando copiar el mismo valor en toda la tabla. 
IONOS
IONOS
 +2
Si la tabla destino no existe y quieres crearla con el nuevo campo, usa SELECT * INTO tabla_nueva FROM tabla_original. 
www.rubenortiz.es
www.rubenortiz.es
 +1


UPDATE chorario_users
SET chorario_users.tipo = chorario_fichajes.tipo
FROM chorario_fichajes
WHERE chorario_users.id = chorario_fichajes.id;

90, 91: intento lo anterior pero da error, así que recurro a forzar en 92 que me dé la sentencia SQL


ALTER TABLE `chorario_fichajes` CHANGE `tipo` `tipo` ENUM('entrada','salida','pausa_inicio','pausa_fin','a','correccion') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;

ALTER TABLE `chorario_fichajes` CHANGE `tipo` `tipo` ENUM('entrada','salida','pausa_desayuno', 'pausa_formación', 'pausa_reunión', 'pausa_médico', 'pausa_inicio', 'pausa_fin', 'vacaciones_inicio', 'vacaciones_fin') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;
… aceptado

ALTER TABLE `chorario_users` CHANGE `tipo` `tipo` ENUM('entrada','salida','pausa_desayuno', 'pausa_formación', 'pausa_reunión', 'pausa_médico', 'pausa_inicio', 'pausa_fin', 'vacaciones_inicio', 'vacaciones_fin') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;
… no lo acepta porque no reconoce el campo tipo (94)

ALTER TABLE `chorario_users` SET `tipo` `tipo` ENUM('entrada','salida','pausa_desayuno', 'pausa_formación', 'pausa_reunión', 'pausa_médico', 'pausa_inicio', 'pausa_fin', 'vacaciones_inicio', 'vacaciones_fin') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;
… y más errores (95)

97: añadir es ADD, ok
ALTER TABLE `chorario_users` ADD `tipo` INT NOT NULL AFTER `full_name`;


102: a ver si avanzo de esta forma... ALTER TABLE `chorario_users` ADD `tipo` ENUM('1','2','3','4') NOT NULL AFTER `full_name`;

103: al fin parece que lo he logrado...
ALTER TABLE `chorario_users` CHANGE `tipo` `tipo` ENUM('entrada','salida','pausa_desayuno', 'pausa_formación', 'pausa_reunión', 'pausa_médico', 'pausa_inicio', 'pausa_fin', 'vacaciones_inicio', 'vacaciones_fin') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;

Ahora tendría que cambiar el código para que el fichaje cambie también el estado del trabajador, sobre 104


https://www.utf8icons.com/favourites
https://www.utf8icons.com/character/9960/black-cross-on-shield


*** Día 24/03/2026


https://chat.deepseek.com/sign_in

seekeseeke96


De ayer:
Quiero añadirle a function fichar(tipo) otro parámetro llamado pausa que añada un valor a un campo llamado pausa, de la tabla chorario_users

Ayer añadí como pude el campo en la tabla chorario_users
Ayer dejé de esta forma la función de fichar:

        function fichar(tipo, npausa) {
            const motivo = tipo === 'correccion' ? prompt('Motivo de corrección:') : '';
            const form = new FormData();
            form.append('tipo', tipo);
            form.append('motivo', motivo);
            form.append('npausa', npausa);

            fetch('api.php?action=fichar', {
                    method: 'POST',
                    body: form
                })
                .then(r => r.json())
                .then(data => {
                    alert(data.mensaje);
                    cargarMisFichajes();
                });
        }


Ahora pasaré a modificar esta parte de api.php:

// Fichar
if ($action === 'fichar') {
    $tipo = $_POST['tipo']; // entrada, salida, pausa_inicio, pausa_fin
    $motivo = $_POST['motivo'] ?? '';

    $stmt = $pdo->prepare("INSERT INTO chorario_fichajes (user_id, tipo, ip, user_agent, motivo) 
                           VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([
        $user_id,
        $tipo,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $motivo
    ]);

    echo json_encode(['success' => true, 'mensaje' => 'Fichaje registrado correctamente']);
}



Luego habría que testear cómo funciona y si acaso pasar a cómo muestra los fichajes:

30: de golpe los 3 que tengo en chorario_users tienen tipo entrada


stmt php que es

$stmt en PHP es una convención de nomenclatura para un objeto que representa una sentencia SQL preparada (prepared statement) en MySQLi o PDO. Se usa para ejecutar consultas de forma segura y eficiente, separando la estructura SQL de los datos para prevenir inyecciones SQL y mejorar el rendimiento en ejecuciones repetidas. 
Stack Overflow
Stack Overflow
 +2
Aspectos clave de $stmt en PHP:

¿Qué hace?: Define una plantilla SQL (prepare), vincula variables (bind_param) y la ejecuta (execute).

Seguridad: Evita la inyección SQL porque los valores se envían por separado de la consulta, por lo que no necesitan comillas manuales.

Eficiencia: La base de datos analiza y optimiza la consulta una sola vez, permitiendo ejecutarla múltiples veces con diferentes datos de forma más rápida.

Métodos comunes:
prepare()`: Prepara la consulta.
bind_param()`: Vincula variables como parámetros.
execute()`: Ejecuta la sentencia.
bind_result()`: Vincula variables para recibir resultados.
fetch()`: Obtiene resultados.
close()`: Cierra la sentencia y libera memoria. 
PHP
PHP
 +7

Ejemplo de uso (MySQLi):
$stmt = $mysqli->prepare("SELECT usuario FROM usuarios WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();


31: la sentencia del cambio es esta:
UPDATE `chorario_users` SET `tipo` = 'salida' WHERE `chorario_users`.`id` = 2;

32: ok, el insert manual coincide con la sentencia que veo en api.php, lo de prepare no influye:
INSERT INTO `chorario_users` (`id`, `username`, `password`, `role`, `full_name`, `tipo`) VALUES ('20', '20', '20', 'employee', '20', 'entrada');
… ah, pero hay que quitarle las comillas simples

Tras resolver una historia de un onerror al lado por '' de un style dentro de "" del onerror...

33: "DELETE FROM chorario_users WHERE `chorario_users`.`id` = 20"

34: por de pronto, a la espera de probarlo, entiendo que esta parte quedaría bien así:

// Fichar
if ($action === 'fichar') {
    $tipo = $_POST['tipo']; // entrada, salida, pausa_inicio, pausa_fin
    $motivo = $_POST['motivo'] ?? '';

    $stmt = $pdo->prepare("INSERT INTO chorario_fichajes (user_id, tipo, ip, user_agent, motivo) 
                           VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([
        $user_id,
        $tipo,
        $_SERVER['REMOTE_ADDR'],
        $_SERVER['HTTP_USER_AGENT'],
        $motivo
    ]);

    $stmt = $pdo->prepare("UPDATE chorario_users SET tipo = ?
                           WHERE chorario_users.id = ?;");
    $stmt->execute([
        $tipo,
        $user_id
    ]);

    echo json_encode(['success' => true, 'mensaje' => 'Fichaje registrado correctamente']);
}



35: https://www.php.net/manual/es/pdo.prepared-statements.php#:~:text=%C2%BFQu%C3%A9%20son?,se%20sigue%20asumiendo%20un%20riesgo).


Tras mencionar location.reload(); vence redirect_url


88: en principio, prueba superada

Me quedaría jugar más con esto, ver cómo muestra los fichajes, configurar lo que se puede marcar o no según el estado actual... y asociar los tipos a números para llevar eso a una tabla aparte


114 es la ventana 2 después de actualizar y quitar los temporales en la 1


https://chat.deepseek.com/

https://chat.deepseek.com/sign_in

https://deep-seek.ai/es/chat no es


*** Día 25/03/2026


<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Botones con contenido dinámico</title>
    <style>
        .contenido { display: none; margin-top: 20px; padding: 15px; border: 1px solid #ccc; }
        .activo { display: block; }
        button { margin: 5px; padding: 10px 15px; }
    </style>
</head>
<body>

    <h2>Elige una opción:</h2>
    
    <button onclick="mostrar('uno')">Botón 1</button>
    <button onclick="mostrar('dos')">Botón 2</button>
    <button onclick="mostrar('tres')">Botón 3</button>

    <!-- Contenidos que se mostrarán uno a la vez -->
    <div id="uno" class="contenido">
        <h3>Contenido del Botón 1</h3>
        <p>Este es el texto que aparece cuando pulsas el Botón 1.</p>
        <?php
            // Aquí puedes poner código PHP que solo se ejecute cuando se muestre este bloque
            echo "<p>Hora actual: " . date('H:i:s') . "</p>";
        ?>
    </div>

    <div id="dos" class="contenido">
        <h3>Contenido del Botón 2</h3>
        <p>Este contenido solo aparece al pulsar el Botón 2.</p>
    </div>

    <div id="tres" class="contenido">
        <h3>Contenido del Botón 3</h3>
        <p>Aquí va el contenido exclusivo del tercer botón.</p>
    </div>

    <script>
        function mostrar(id) {
            // Ocultar todos los contenidos
            document.querySelectorAll('.contenido').forEach(div => {
                div.classList.remove('activo');
            });

            // Mostrar solo el que queremos
            document.getElementById(id).classList.add('activo');
        }
    </script>

</body>
</html>


Esto era sólo en PHP, aunque me decía que lo mejor era lo anterior: PHP + JS

<?php
$mostrar = $_GET['mostrar'] ?? ''; // Recoge el parámetro de la URL
?>

<button onclick="location.href='?mostrar=uno'">Botón 1</button>
<button onclick="location.href='?mostrar=dos'">Botón 2</button>
<button onclick="location.href='?mostrar=tres'">Botón 3</button>

<?php if ($mostrar === 'uno'): ?>
    <div style="margin-top:20px; padding:15px; border:1px solid blue;">
        <h3>Contenido del Botón 1</h3>
        <p>Este es el contenido del primero.</p>
    </div>
<?php elseif ($mostrar === 'dos'): ?>
    <div style="margin-top:20px; padding:15px; border:1px solid green;">
        <h3>Contenido del Botón 2</h3>
    </div>
<?php elseif ($mostrar === 'tres'): ?>
    <div style="margin-top:20px; padding:15px; border:1px solid red;">
        <h3>Contenido del Botón 3</h3>
    </div>
<?php endif; ?>



Análisis de expresiones:
            // Ocultar todos los contenidos
            document.querySelectorAll('.contenido').forEach(div => {
                div.classList.remove('activo');
            });

            // Mostrar solo el que queremos
            document.querySelectorAll('.contenido').forEach(div => {div.classList.remove('activo');

Esta línea de JavaScript se utiliza para eliminar la clase CSS 'activo' de todos los elementos HTML que tengan la clase 'contenido' en una página web. 
GitHub Pages documentation
GitHub Pages documentation
 +1
Aquí tienes el desglose detallado:

document.querySelectorAll('.contenido'): Busca en todo el documento HTML todos los elementos (divs, en este caso) que tengan la clase CSS .contenido. Devuelve una lista de estos elementos (una NodeList).

.forEach(div => { ... }): Recorre cada uno de los elementos encontrados uno por uno. A cada elemento individual lo llama div.

div.classList.remove('activo');: Para cada elemento encontrado (div), utiliza la propiedad classList.remove() para quitarle la clase CSS 'activo' si la tenía. 
GitHub Pages documentation
GitHub Pages documentation
 +4
En resumen:
Si tienes varios elementos con la clase .contenido y algunos de ellos están activos (por ejemplo, visibles o resaltados), esta instrucción los "desactiva" todos al mismo tiempo, usualmente para preparar la interfaz y activar solo uno nuevo después. Es muy común en menús, pestañas (tabs) o galerías de imágenes.





            // Mostrar solo el que queremos
            document.getElementById(id).classList.add('activo');


La línea de código document.getElementById(id).classList.add('activo'); en JavaScript sirve para añadir la clase CSS 'activo' a un elemento HTML específico que tiene un id determinado. 
Aquí está el desglose de lo que hace cada parte:

document.getElementById(id): Busca y selecciona en el documento HTML el elemento único que tiene el id especificado (donde id es el nombre de la variable o la cadena de texto del identificador).

.classList: Accede a la lista de clases CSS del elemento seleccionado.

.add('activo'): Incorpora la clase 'activo' a la lista existente sin eliminar las clases que ya tuviera el elemento. 
El Tutorial de JavaScript Moderno
El Tutorial de JavaScript Moderno
 +4


Ejemplo práctico:
Si tienes este HTML:
html
<div id="miElemento" class="caja">Hola</div>

Al ejecutar:
javascript
document.getElementById('miElemento').classList.add('activo');

El HTML cambiará automáticamente a:
html
<div id="miElemento" class="caja activo">Hola</div>
Esto se utiliza comúnmente para cambiar estilos de forma dinámica (por ejemplo, cambiar el color, mostrar un elemento oculto, etc.) mediante CSS cuando ocurre un evento. 
YouTube
YouTube


div con varias clases

Para asignar varias clases a un div en HTML, sepáralas por un espacio dentro del atributo class, permitiendo combinar estilos de CSS. Ejemplo: <div class="clase1 clase2">Contenido</div>. Esto aplica las propiedades de ambas clases, siendo útil para modularizar el diseño. Se recomienda limitar su uso excesivo para mantener la semántica. 
Mclibre.org
Mclibre.org
 +4
Ejemplo Práctico:

HTML:
html
<div class="contenedor borde-rojo fondo-gris">
  Contenido con múltiples estilos.
</div>

CSS:
css
.contenedor { width: 100%; padding: 20px; }
.borde-rojo { border: 2px solid red; }
.fondo-gris { background-color: #ccc; }


Puntos Clave:
Separación: Usa espacios, no comas, para separar los nombres de las clases.

Orden: El orden en el HTML no importa, lo que prevalece es el orden de definición en el CSS.

JavaScript: Para añadir clases dinámicamente, usa elemento.classList.add('nueva-clase').

Especificidad: Si dos clases modifican la misma propiedad, el navegador decidirá cuál aplicar basándose en la especificidad CSS. 
Mclibre.org
Mclibre.org
 +3



1. Filtrar al generar contenido (Recomendado)
Si estás obteniendo datos de una base de datos (por ejemplo, mysqli), utiliza una estructura condicional if para decidir si imprimes el div con la clase específica.
php
<?php
// Supongamos que tienes un array de datos
$productos = [
    ['nombre' => 'Producto 1', 'tipo' => 'especial'],
    ['nombre' => 'Producto 2', 'tipo' => 'normal'],
    ['nombre' => 'Producto 3', 'tipo' => 'especial'],
];

foreach ($productos as $producto) {
    // Solo mostrar si el tipo es 'especial'
    if ($producto['tipo'] === 'especial') {
        echo '<div class="especial">' . $producto['nombre'] . '</div>';
    }
}
?>


2. Ocultar mediante CSS controlado por PHP
Puedes usar PHP para añadir una clase CSS que oculte el elemento (display: none) si no cumple la condición.
php
<?php
$mostrarSoloClaseX = true; // Condición lógica
$claseDiv = ($mostrarSoloClaseX) ? 'mostrar' : 'ocultar';
?>

<style>
    .ocultar { display: none; }
    .mostrar { display: block; }
</style>

<div class="mi-clase-concreta <?php echo $claseDiv; ?>">
    Contenido condicional
</div>




Generar un if con un OR para agrupar los botones que dan derecho a X botones, y jugar con lo del display block o none





Las variables definidas dentro de una función en PHP no se trasladan automáticamente de un fichero a otro. En PHP, las variables dentro de funciones tienen un alcance local (local scope). 
Codefinity
Codefinity
 +2
Para compartir variables entre ficheros PHP, existen métodos específicos dependiendo de si necesitas la variable en el ámbito global o dentro de otra función. 

1. Variables dentro de funciones (Ámbito Local)
Una variable creada dentro de function A() en fichero1.php no existe fuera de ella, y mucho menos en fichero2.php. Para trasladar su valor:
Retornar valor (Recomendado): La función debe return el valor, y luego guardar ese resultado en una nueva variable en el segundo fichero.
php
// fichero1.php
function obtenerDatos() {
    $variableLocal = "Hola";
    return $variableLocal;
}

// fichero2.php
require 'fichero1.php';
$recibido = obtenerDatos();
echo $recibido; // "Hola"

2. Variables fuera de funciones (Ámbito Global)
Si la variable está definida fuera de cualquier función, al usar include o require, el fichero incluido hereda el ámbito (scope) de donde se llamó. 
PHP
PHP
Uso de include/require:
php
// vars.php
$color = 'verde';

// principal.php
include 'vars.php';
echo $color; // "verde"
 
3. Métodos para pasar datos entre ficheros
Sesiones ($_SESSION): Ideal para mantener datos del usuario entre páginas.
Parámetros GET/POST: Útil para enviar datos a través de enlaces (?id=1) o formularios.
Palabra clave global: Permite acceder a una variable global desde dentro de una función, útil si incluyes un fichero con global $var. 
Stack Overflow
Stack Overflow
 +3
En resumen: Las variables locales de funciones son privadas. Debes retornar su valor o utilizar mecanismos de almacenamiento como sesiones o peticiones HTTP para trasladarlas. 
Quora
Quora
 +3


Por tanto, usaré un $_SESSION del tipo este que he creado, y lo iré modificando en cada fichaje



56: https://es.stackoverflow.com/questions/144112/obtener-variable-php-desde-otro-archivo

59: tras restaurar pestañas no me saca el contenido de antes que había traducido


*** Día 26/03/2026


El trabajador debe poder sacar sus listados, puedo ir por allí

20: que cada trabajador pueda ver su estado (sobre los botones, por ejemplo)

27, 28: entiendo que la etiqueta <h5> funciona como un bloque... Se me ocurre o poner ese texto antes o usar un <h6>
29, 30: correcto, funciona. Lo pondré como un <h5> también para igualar estilos

32: estado inicial de esa parte antes de meterle botoncitos al empleado
33: ¿el empleado me sale como employee????


40: línea 95, si ya hemos llegado hasta aquí por ser admin no tiene sentido repetirlo, pienso yo

41: tengo un error... 42: a ver si con esto me queda resuelto

44, 45: que no, que la parte empleado no me va

47: a ver si pasando de WHERE $_SESSION[user_id] a WHERE f.user_id = $_SESSION[user_id] funciona...

Cambios que Alfonso hizo a partir de feedback con otras empresas:
repositorio de nóminas
marcar festivos de empresa
comunicar mensajes a trabajadores

48: pues no, sigue fallando... 49, 50: ah, claro, que he metido el where delante del join, de forma inconsciente

58: lo que no se ve es esto:
<b>Fatal error</b>:  Uncaught PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'JOIN chorario_users u ON f.user_id = u.id 

59: corregido, espero

60, 61: el reporte va bien, perfecto
Al final no ha habido que meter ? en el prepare y luego el campo en el $stmt->execute();... ¿tal vez sería mejor con el interrogante, por el tema de la protección de la web??? (rev, duda)
62: … el CSV todavía falla

63: parto de esto: if ($action === 'exportare' && $_SESSION['role'] === 'admin') {
cuando no es admin sino employee… a cambiar

65 a 67: conseguido


*** Día 27/03/2026


We really appreciate people trying our new features as soon as they are ready, so check back here often and learn what's new.
If you'd like to read release notes for previous VS Code versions, go to Updates on code.visualstudio.com.


mensaje bot lleno visual studio code

Si estás recibiendo un mensaje de "bot lleno", "límite alcanzado" o similar en Visual Studio Code, probablemente se deba a restricciones en el uso de extensiones de IA (como Bito, Codeium o GitHub Copilot) o a un exceso de procesos en la terminal.
Aquí tienes los pasos para solucionar este problema:
1. Si es un Bot de IA (Bito, Copilot, Gemini)
Esperar a que se reinicie el ciclo: Muchas extensiones ofrecen un número limitado de interacciones gratuitas al día o al mes. El mensaje suele desaparecer al día siguiente.

Cerrar sesión y reiniciar VS Code: A veces, el error es de autenticación. Cierra sesión en la extensión y vuelve a iniciarla.

Revisar uso de recursos: Si usas agentes locales de IA (como en n8n), verifica que no hayas saturado la memoria de tu servidor con demasiadas peticiones simultáneas.

Ocultar/Desactivar funciones: Si la IA es muy intrusiva, puedes ocultarla presionando CTRL + SHIFT + P y escribiendo "Ocultar funciones de IA" (Hide AI Features). 
YouTube
YouTube
 +4

2. Si la Terminal de VS Code está llena de mensajes
Si la terminal muestra errores repetitivos o "basura" que impide trabajar:
Limpiar la terminal: Escribe clear y presiona Enter en la terminal (Linux/Mac/Git Bash). En Windows PowerShell, usa cls.

Reactivar terminales (Error común): Si aparece un error constante de "reactivating terminals", ve a la configuración (Ctrl+,), busca "Python Locator" y cambia la opción a js, luego reinicia VS Code. 
YouTube
YouTube
 +1

3. Soluciones Técnicas Rápidas
Reiniciar Visual Studio Code: La solución más rápida para congelamientos de la IA.

Actualizar/Reinstalar la extensión: Si el bot sigue dando error, desinstala y vuelve a instalar la extensión desde el Marketplace. 
YouTube
YouTube
 +1
Si se está utilizando una herramienta específica (ej. Bito), asegúrate de revisar su panel de configuración para verificar el límite de uso actual. 
YouTube
YouTube


bootstrap que es

Bootstrap es un popular framework front-end de código abierto, diseñado por Twitter, que facilita la creación rápida y eficiente de sitios web responsivos y adaptables a dispositivos móviles. Proporciona una biblioteca de componentes predefinidos (botones, menús, formularios) y un sistema de rejilla (grid system) basados en HTML, CSS y JavaScript. 
Santander Open Academy
Santander Open Academy
 +3

Características Principales:
Diseño Responsive: Permite que las páginas web se adapten automáticamente a diferentes tamaños de pantalla, desde ordenadores hasta dispositivos móviles.

Componentes Predefinidos: Incluye elementos estilizados listos para usar, como barras de navegación, carruseles, ventanas modales y alertas.

Sistema de Rejilla (Grid): Facilita la maquetación de la web utilizando una estructura de 12 columnas que organiza el contenido de manera ordenada.

Fácil de Usar: Utiliza clases CSS predefinidas que se aplican a los elementos HTML para asignarles estilos rápidamente. 
YouTube
YouTube
 +4
Este video explica qué es Bootstrap y por qué sigue siendo una herramienta popular en el desarrollo web:



Learn Code
YouTube• 12 may 2025
Ventajas de usar Bootstrap:

Ahorro de tiempo: Al contar con estilos y componentes listos, no es necesario escribir todo el código CSS desde cero.

Consistencia: Asegura que el diseño sea uniforme en todo el sitio web.

Compatibilidad: Funciona correctamente en la mayoría de los navegadores modernos (Chrome, Firefox, Safari, Edge). 
Santander Open Academy
Santander Open Academy
 +4

Bootstrap es ideal para desarrolladores que buscan crear sitios web con un diseño atractivo y moderno de manera ágil. 
Arsys
Arsys
 +1



IAs de programación: Zencoder, Antigravity, Claude, Deepseek











